github.com/lunarobliq/gophish@v0.8.1-0.20230523153303-93511002234d/static/js/src/app/campaign_results.js (about) 1 var map = null 2 var doPoll = true; 3 4 // statuses is a helper map to point result statuses to ui classes 5 var statuses = { 6 "Email Sent": { 7 color: "#1abc9c", 8 label: "label-success", 9 icon: "fa-envelope", 10 point: "ct-point-sent" 11 }, 12 "Emails Sent": { 13 color: "#1abc9c", 14 label: "label-success", 15 icon: "fa-envelope", 16 point: "ct-point-sent" 17 }, 18 "In progress": { 19 label: "label-primary" 20 }, 21 "Queued": { 22 label: "label-info" 23 }, 24 "Completed": { 25 label: "label-success" 26 }, 27 "Email Opened": { 28 color: "#f9bf3b", 29 label: "label-warning", 30 icon: "fa-envelope-open", 31 point: "ct-point-opened" 32 }, 33 "Clicked Link": { 34 color: "#F39C12", 35 label: "label-clicked", 36 icon: "fa-mouse-pointer", 37 point: "ct-point-clicked" 38 }, 39 "Success": { 40 color: "#f05b4f", 41 label: "label-danger", 42 icon: "fa-exclamation", 43 point: "ct-point-clicked" 44 }, 45 //not a status, but is used for the campaign timeline and user timeline 46 "Email Reported": { 47 color: "#45d6ef", 48 label: "label-info", 49 icon: "fa-bullhorn", 50 point: "ct-point-reported" 51 }, 52 "Error": { 53 color: "#6c7a89", 54 label: "label-default", 55 icon: "fa-times", 56 point: "ct-point-error" 57 }, 58 "Error Sending Email": { 59 color: "#6c7a89", 60 label: "label-default", 61 icon: "fa-times", 62 point: "ct-point-error" 63 }, 64 "Submitted Data": { 65 color: "#f05b4f", 66 label: "label-danger", 67 icon: "fa-exclamation", 68 point: "ct-point-clicked" 69 }, 70 "Unknown": { 71 color: "#6c7a89", 72 label: "label-default", 73 icon: "fa-question", 74 point: "ct-point-error" 75 }, 76 "Sending": { 77 color: "#428bca", 78 label: "label-primary", 79 icon: "fa-spinner", 80 point: "ct-point-sending" 81 }, 82 "Retrying": { 83 color: "#6c7a89", 84 label: "label-default", 85 icon: "fa-clock-o", 86 point: "ct-point-error" 87 }, 88 "Scheduled": { 89 color: "#428bca", 90 label: "label-primary", 91 icon: "fa-clock-o", 92 point: "ct-point-sending" 93 }, 94 "Campaign Created": { 95 label: "label-success", 96 icon: "fa-rocket" 97 } 98 } 99 100 var statusMapping = { 101 "Email Sent": "sent", 102 "Email Opened": "opened", 103 "Clicked Link": "clicked", 104 "Submitted Data": "submitted_data", 105 "Email Reported": "reported", 106 } 107 108 // This is an underwhelming attempt at an enum 109 // until I have time to refactor this appropriately. 110 var progressListing = [ 111 "Email Sent", 112 "Email Opened", 113 "Clicked Link", 114 "Submitted Data" 115 ] 116 117 var campaign = {} 118 var bubbles = [] 119 120 function dismiss() { 121 $("#modal\\.flashes").empty() 122 $("#modal").modal('hide') 123 $("#resultsTable").dataTable().DataTable().clear().draw() 124 } 125 126 // Deletes a campaign after prompting the user 127 function deleteCampaign() { 128 Swal.fire({ 129 title: "Are you sure?", 130 text: "This will delete the campaign. This can't be undone!", 131 type: "warning", 132 animation: false, 133 showCancelButton: true, 134 confirmButtonText: "Delete Campaign", 135 confirmButtonColor: "#428bca", 136 reverseButtons: true, 137 allowOutsideClick: false, 138 showLoaderOnConfirm: true, 139 preConfirm: function () { 140 return new Promise(function (resolve, reject) { 141 api.campaignId.delete(campaign.id) 142 .success(function (msg) { 143 resolve() 144 }) 145 .error(function (data) { 146 reject(data.responseJSON.message) 147 }) 148 }) 149 } 150 }).then(function (result) { 151 if(result.value){ 152 Swal.fire( 153 'Campaign Deleted!', 154 'This campaign has been deleted!', 155 'success' 156 ); 157 } 158 $('button:contains("OK")').on('click', function () { 159 location.href = '/campaigns' 160 }) 161 }) 162 } 163 164 // Completes a campaign after prompting the user 165 function completeCampaign() { 166 Swal.fire({ 167 title: "Are you sure?", 168 text: "Gophish will stop processing events for this campaign", 169 type: "warning", 170 animation: false, 171 showCancelButton: true, 172 confirmButtonText: "Complete Campaign", 173 confirmButtonColor: "#428bca", 174 reverseButtons: true, 175 allowOutsideClick: false, 176 showLoaderOnConfirm: true, 177 preConfirm: function () { 178 return new Promise(function (resolve, reject) { 179 api.campaignId.complete(campaign.id) 180 .success(function (msg) { 181 resolve() 182 }) 183 .error(function (data) { 184 reject(data.responseJSON.message) 185 }) 186 }) 187 } 188 }).then(function (result) { 189 if (result.value){ 190 Swal.fire( 191 'Campaign Completed!', 192 'This campaign has been completed!', 193 'success' 194 ); 195 $('#complete_button')[0].disabled = true; 196 $('#complete_button').text('Completed!') 197 doPoll = false; 198 } 199 }) 200 } 201 202 // Exports campaign results as a CSV file 203 function exportAsCSV(scope) { 204 exportHTML = $("#exportButton").html() 205 var csvScope = null 206 var filename = campaign.name + ' - ' + capitalize(scope) + '.csv' 207 switch (scope) { 208 case "results": 209 csvScope = campaign.results 210 break; 211 case "events": 212 csvScope = campaign.timeline 213 break; 214 } 215 if (!csvScope) { 216 return 217 } 218 $("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>') 219 var csvString = Papa.unparse(csvScope, { 220 'escapeFormulae': true 221 }) 222 var csvData = new Blob([csvString], { 223 type: 'text/csv;charset=utf-8;' 224 }); 225 if (navigator.msSaveBlob) { 226 navigator.msSaveBlob(csvData, filename); 227 } else { 228 var csvURL = window.URL.createObjectURL(csvData); 229 var dlLink = document.createElement('a'); 230 dlLink.href = csvURL; 231 dlLink.setAttribute('download', filename) 232 document.body.appendChild(dlLink) 233 dlLink.click(); 234 document.body.removeChild(dlLink) 235 } 236 $("#exportButton").html(exportHTML) 237 } 238 239 function replay(event_idx) { 240 request = campaign.timeline[event_idx] 241 details = JSON.parse(request.details) 242 url = null 243 form = $('<form>').attr({ 244 method: 'POST', 245 target: '_blank', 246 }) 247 /* Create a form object and submit it */ 248 $.each(Object.keys(details.payload), function (i, param) { 249 if (param == "rid") { 250 return true; 251 } 252 if (param == "__original_url") { 253 url = details.payload[param]; 254 return true; 255 } 256 $('<input>').attr({ 257 name: param, 258 }).val(details.payload[param]).appendTo(form); 259 }) 260 /* Ensure we know where to send the user */ 261 // Prompt for the URL 262 Swal.fire({ 263 title: 'Where do you want the credentials submitted to?', 264 input: 'text', 265 showCancelButton: true, 266 inputPlaceholder: "http://example.com/login", 267 inputValue: url || "", 268 inputValidator: function (value) { 269 return new Promise(function (resolve, reject) { 270 if (value) { 271 resolve(); 272 } else { 273 reject('Invalid URL.'); 274 } 275 }); 276 } 277 }).then(function (result) { 278 if (result.value){ 279 url = result.value 280 submitForm() 281 } 282 }) 283 return 284 submitForm() 285 286 function submitForm() { 287 form.attr({ 288 action: url 289 }) 290 form.appendTo('body').submit().remove() 291 } 292 } 293 294 /** 295 * Returns an HTML string that displays the OS and browser that clicked the link 296 * or submitted credentials. 297 * 298 * @param {object} event_details - The "details" parameter for a campaign 299 * timeline event 300 * 301 */ 302 var renderDevice = function (event_details) { 303 var ua = UAParser(details.browser['user-agent']) 304 var detailsString = '<div class="timeline-device-details">' 305 306 var deviceIcon = 'laptop' 307 if (ua.device.type) { 308 if (ua.device.type == 'tablet' || ua.device.type == 'mobile') { 309 deviceIcon = ua.device.type 310 } 311 } 312 313 var deviceVendor = '' 314 if (ua.device.vendor) { 315 deviceVendor = ua.device.vendor.toLowerCase() 316 if (deviceVendor == 'microsoft') deviceVendor = 'windows' 317 } 318 319 var deviceName = 'Unknown' 320 if (ua.os.name) { 321 deviceName = ua.os.name 322 if (deviceName == "Mac OS") { 323 deviceVendor = 'apple' 324 } else if (deviceName == "Windows") { 325 deviceVendor = 'windows' 326 } 327 if (ua.device.vendor && ua.device.model) { 328 deviceName = ua.device.vendor + ' ' + ua.device.model 329 } 330 } 331 332 if (ua.os.version) { 333 deviceName = deviceName + ' (OS Version: ' + ua.os.version + ')' 334 } 335 336 deviceString = '<div class="timeline-device-os"><span class="fa fa-stack">' + 337 '<i class="fa fa-' + escapeHtml(deviceIcon) + ' fa-stack-2x"></i>' + 338 '<i class="fa fa-vendor-icon fa-' + escapeHtml(deviceVendor) + ' fa-stack-1x"></i>' + 339 '</span> ' + escapeHtml(deviceName) + '</div>' 340 341 detailsString += deviceString 342 343 var deviceBrowser = 'Unknown' 344 var browserIcon = 'info-circle' 345 var browserVersion = '' 346 347 if (ua.browser && ua.browser.name) { 348 deviceBrowser = ua.browser.name 349 // Handle the "mobile safari" case 350 deviceBrowser = deviceBrowser.replace('Mobile ', '') 351 if (deviceBrowser) { 352 browserIcon = deviceBrowser.toLowerCase() 353 if (browserIcon == 'ie') browserIcon = 'internet-explorer' 354 } 355 browserVersion = '(Version: ' + ua.browser.version + ')' 356 } 357 358 var browserString = '<div class="timeline-device-browser"><span class="fa fa-stack">' + 359 '<i class="fa fa-' + escapeHtml(browserIcon) + ' fa-stack-1x"></i></span> ' + 360 deviceBrowser + ' ' + browserVersion + '</div>' 361 362 detailsString += browserString 363 detailsString += '</div>' 364 return detailsString 365 } 366 367 function renderTimeline(data) { 368 record = { 369 "id": data[0], 370 "first_name": data[2], 371 "last_name": data[3], 372 "email": data[4], 373 "position": data[5], 374 "status": data[6], 375 "reported": data[7], 376 "send_date": data[8] 377 } 378 results = '<div class="timeline col-sm-12 well well-lg">' + 379 '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + 380 '</h6><span class="subtitle">Email: ' + escapeHtml(record.email) + 381 '<br>Result ID: ' + escapeHtml(record.id) + '</span>' + 382 '<div class="timeline-graph col-sm-6">' 383 $.each(campaign.timeline, function (i, event) { 384 if (!event.email || event.email == record.email) { 385 // Add the event 386 results += '<div class="timeline-entry">' + 387 ' <div class="timeline-bar"></div>' 388 results += 389 ' <div class="timeline-icon ' + statuses[event.message].label + '">' + 390 ' <i class="fa ' + statuses[event.message].icon + '"></i></div>' + 391 ' <div class="timeline-message">' + escapeHtml(event.message) + 392 ' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>' 393 if (event.details) { 394 details = JSON.parse(event.details) 395 if (event.message == "Clicked Link" || event.message == "Submitted Data") { 396 deviceView = renderDevice(details) 397 if (deviceView) { 398 results += deviceView 399 } 400 } 401 if (event.message == "Submitted Data") { 402 results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">' 403 results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>' 404 results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>' 405 } 406 if (details.payload) { 407 results += '<div class="timeline-event-results">' 408 results += ' <table class="table table-condensed table-bordered table-striped">' 409 results += ' <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>' 410 $.each(Object.keys(details.payload), function (i, param) { 411 if (param == "rid") { 412 return true; 413 } 414 results += ' <tr>' 415 results += ' <td>' + escapeHtml(param) + '</td>' 416 results += ' <td>' + escapeHtml(details.payload[param]) + '</td>' 417 results += ' </tr>' 418 }) 419 results += ' </tbody></table>' 420 results += '</div>' 421 } 422 if (details.error) { 423 results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>' 424 results += '<div class="timeline-event-results">' 425 results += '<span class="label label-default">Error</span> ' + details.error 426 results += '</div>' 427 } 428 } 429 results += '</div></div>' 430 } 431 }) 432 // Add the scheduled send event at the bottom 433 if (record.status == "Scheduled" || record.status == "Retrying") { 434 results += '<div class="timeline-entry">' + 435 ' <div class="timeline-bar"></div>' 436 results += 437 ' <div class="timeline-icon ' + statuses[record.status].label + '">' + 438 ' <i class="fa ' + statuses[record.status].icon + '"></i></div>' + 439 ' <div class="timeline-message">' + "Scheduled to send at " + record.send_date + '</span>' 440 } 441 results += '</div></div>' 442 return results 443 } 444 445 var renderTimelineChart = function (chartopts) { 446 return Highcharts.chart('timeline_chart', { 447 chart: { 448 zoomType: 'x', 449 type: 'line', 450 height: "200px" 451 }, 452 title: { 453 text: 'Campaign Timeline' 454 }, 455 xAxis: { 456 type: 'datetime', 457 dateTimeLabelFormats: { 458 second: '%l:%M:%S', 459 minute: '%l:%M', 460 hour: '%l:%M', 461 day: '%b %d, %Y', 462 week: '%b %d, %Y', 463 month: '%b %Y' 464 } 465 }, 466 yAxis: { 467 min: 0, 468 max: 2, 469 visible: false, 470 tickInterval: 1, 471 labels: { 472 enabled: false 473 }, 474 title: { 475 text: "" 476 } 477 }, 478 tooltip: { 479 formatter: function () { 480 return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) + 481 '<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>' 482 } 483 }, 484 legend: { 485 enabled: false 486 }, 487 plotOptions: { 488 series: { 489 marker: { 490 enabled: true, 491 symbol: 'circle', 492 radius: 3 493 }, 494 cursor: 'pointer', 495 }, 496 line: { 497 states: { 498 hover: { 499 lineWidth: 1 500 } 501 } 502 } 503 }, 504 credits: { 505 enabled: false 506 }, 507 series: [{ 508 data: chartopts['data'], 509 dashStyle: "shortdash", 510 color: "#cccccc", 511 lineWidth: 1, 512 turboThreshold: 0 513 }] 514 }) 515 } 516 517 /* Renders a pie chart using the provided chartops */ 518 var renderPieChart = function (chartopts) { 519 return Highcharts.chart(chartopts['elemId'], { 520 chart: { 521 type: 'pie', 522 events: { 523 load: function () { 524 var chart = this, 525 rend = chart.renderer, 526 pie = chart.series[0], 527 left = chart.plotLeft + pie.center[0], 528 top = chart.plotTop + pie.center[1]; 529 this.innerText = rend.text(chartopts['data'][0].count, left, top). 530 attr({ 531 'text-anchor': 'middle', 532 'font-size': '24px', 533 'font-weight': 'bold', 534 'fill': chartopts['colors'][0], 535 'font-family': 'Helvetica,Arial,sans-serif' 536 }).add(); 537 }, 538 render: function () { 539 this.innerText.attr({ 540 text: chartopts['data'][0].count 541 }) 542 } 543 } 544 }, 545 title: { 546 text: chartopts['title'] 547 }, 548 plotOptions: { 549 pie: { 550 innerSize: '80%', 551 dataLabels: { 552 enabled: false 553 } 554 } 555 }, 556 credits: { 557 enabled: false 558 }, 559 tooltip: { 560 formatter: function () { 561 if (this.key == undefined) { 562 return false 563 } 564 return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '%</b><br/>' 565 } 566 }, 567 series: [{ 568 data: chartopts['data'], 569 colors: chartopts['colors'], 570 }] 571 }) 572 } 573 574 /* Updates the bubbles on the map 575 576 @param {campaign.result[]} results - The campaign results to process 577 */ 578 var updateMap = function (results) { 579 if (!map) { 580 return 581 } 582 bubbles = [] 583 $.each(campaign.results, function (i, result) { 584 // Check that it wasn't an internal IP 585 if (result.latitude == 0 && result.longitude == 0) { 586 return true; 587 } 588 newIP = true 589 $.each(bubbles, function (i, bubble) { 590 if (bubble.ip == result.ip) { 591 bubbles[i].radius += 1 592 newIP = false 593 return false 594 } 595 }) 596 if (newIP) { 597 bubbles.push({ 598 latitude: result.latitude, 599 longitude: result.longitude, 600 name: result.ip, 601 fillKey: "point", 602 radius: 2 603 }) 604 } 605 }) 606 map.bubbles(bubbles) 607 } 608 609 /** 610 * Creates a status label for use in the results datatable 611 * @param {string} status 612 * @param {moment(datetime)} send_date 613 */ 614 function createStatusLabel(status, send_date) { 615 var label = statuses[status].label || "label-default"; 616 var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>" 617 // Add the tooltip if the email is scheduled to be sent 618 if (status == "Scheduled" || status == "Retrying") { 619 var sendDateMessage = "Scheduled to send at " + send_date 620 statusColumn = "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"top\" data-html=\"true\" title=\"" + sendDateMessage + "\">" + status + "</span>" 621 } 622 return statusColumn 623 } 624 625 /* poll - Queries the API and updates the UI with the results 626 * 627 * Updates: 628 * * Timeline Chart 629 * * Email (Donut) Chart 630 * * Map Bubbles 631 * * Datatables 632 */ 633 function poll() { 634 api.campaignId.results(campaign.id) 635 .success(function (c) { 636 campaign = c 637 /* Update the timeline */ 638 var timeline_series_data = [] 639 $.each(campaign.timeline, function (i, event) { 640 var event_date = moment.utc(event.time).local() 641 timeline_series_data.push({ 642 email: event.email, 643 message: event.message, 644 x: event_date.valueOf(), 645 y: 1, 646 marker: { 647 fillColor: statuses[event.message].color 648 } 649 }) 650 }) 651 var timeline_chart = $("#timeline_chart").highcharts() 652 timeline_chart.series[0].update({ 653 data: timeline_series_data 654 }) 655 /* Update the results donut chart */ 656 var email_series_data = {} 657 // Load the initial data 658 Object.keys(statusMapping).forEach(function (k) { 659 email_series_data[k] = 0 660 }); 661 $.each(campaign.results, function (i, result) { 662 email_series_data[result.status]++; 663 if (result.reported) { 664 email_series_data['Email Reported']++ 665 } 666 // Backfill status values 667 var step = progressListing.indexOf(result.status) 668 for (var i = 0; i < step; i++) { 669 email_series_data[progressListing[i]]++ 670 } 671 }) 672 $.each(email_series_data, function (status, count) { 673 var email_data = [] 674 if (!(status in statusMapping)) { 675 return true 676 } 677 email_data.push({ 678 name: status, 679 y: Math.floor((count / campaign.results.length) * 100), 680 count: count 681 }) 682 email_data.push({ 683 name: '', 684 y: 100 - Math.floor((count / campaign.results.length) * 100) 685 }) 686 var chart = $("#" + statusMapping[status] + "_chart").highcharts() 687 chart.series[0].update({ 688 data: email_data 689 }) 690 }) 691 692 /* Update the datatable */ 693 resultsTable = $("#resultsTable").DataTable() 694 resultsTable.rows().every(function (i, tableLoop, rowLoop) { 695 var row = this.row(i) 696 var rowData = row.data() 697 var rid = rowData[0] 698 $.each(campaign.results, function (j, result) { 699 if (result.id == rid) { 700 rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') 701 rowData[7] = result.reported 702 rowData[6] = result.status 703 resultsTable.row(i).data(rowData) 704 if (row.child.isShown()) { 705 $(row.node()).find("#caret").removeClass("fa-caret-right") 706 $(row.node()).find("#caret").addClass("fa-caret-down") 707 row.child(renderTimeline(row.data())) 708 } 709 return false 710 } 711 }) 712 }) 713 resultsTable.draw(false) 714 /* Update the map information */ 715 updateMap(campaign.results) 716 $('[data-toggle="tooltip"]').tooltip() 717 $("#refresh_message").hide() 718 $("#refresh_btn").show() 719 }) 720 } 721 722 function load() { 723 campaign.id = window.location.pathname.split('/').slice(-1)[0] 724 var use_map = JSON.parse(localStorage.getItem('gophish.use_map')) 725 api.campaignId.results(campaign.id) 726 .success(function (c) { 727 campaign = c 728 if (campaign) { 729 $("title").text(c.name + " - Gophish") 730 $("#loading").hide() 731 $("#campaignResults").show() 732 // Set the title 733 $("#page-title").text("Results for " + c.name) 734 if (c.status == "Completed") { 735 $('#complete_button')[0].disabled = true; 736 $('#complete_button').text('Completed!'); 737 doPoll = false; 738 } 739 // Setup viewing the details of a result 740 $("#resultsTable").on("click", ".timeline-event-details", function () { 741 // Show the parameters 742 payloadResults = $(this).parent().find(".timeline-event-results") 743 if (payloadResults.is(":visible")) { 744 $(this).find("i").removeClass("fa-caret-down") 745 $(this).find("i").addClass("fa-caret-right") 746 payloadResults.hide() 747 } else { 748 $(this).find("i").removeClass("fa-caret-right") 749 $(this).find("i").addClass("fa-caret-down") 750 payloadResults.show() 751 } 752 }) 753 // Setup the results table 754 resultsTable = $("#resultsTable").DataTable({ 755 destroy: true, 756 "order": [ 757 [2, "asc"] 758 ], 759 columnDefs: [{ 760 orderable: false, 761 targets: "no-sort" 762 }, { 763 className: "details-control", 764 "targets": [1] 765 }, { 766 "visible": false, 767 "targets": [0, 8] 768 }, 769 { 770 "render": function (data, type, row) { 771 return createStatusLabel(data, row[8]) 772 }, 773 "targets": [6] 774 }, 775 { 776 className: "text-center", 777 "render": function (reported, type, row) { 778 if (type == "display") { 779 if (reported) { 780 return "<i class='fa fa-check-circle text-center text-success'></i>" 781 } 782 return "<i role='button' class='fa fa-times-circle text-center text-muted' onclick='report_mail(\"" + row[0] + "\", \"" + campaign.id + "\");'></i>" 783 } 784 return reported 785 }, 786 "targets": [7] 787 } 788 ] 789 }); 790 resultsTable.clear(); 791 var email_series_data = {} 792 var timeline_series_data = [] 793 Object.keys(statusMapping).forEach(function (k) { 794 email_series_data[k] = 0 795 }); 796 $.each(campaign.results, function (i, result) { 797 resultsTable.row.add([ 798 result.id, 799 "<i id=\"caret\" class=\"fa fa-caret-right\"></i>", 800 escapeHtml(result.first_name) || "", 801 escapeHtml(result.last_name) || "", 802 escapeHtml(result.email) || "", 803 escapeHtml(result.position) || "", 804 result.status, 805 result.reported, 806 moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') 807 ]) 808 email_series_data[result.status]++; 809 if (result.reported) { 810 email_series_data['Email Reported']++ 811 } 812 // Backfill status values 813 var step = progressListing.indexOf(result.status) 814 for (var i = 0; i < step; i++) { 815 email_series_data[progressListing[i]]++ 816 } 817 }) 818 resultsTable.draw(); 819 // Setup tooltips 820 $('[data-toggle="tooltip"]').tooltip() 821 // Setup the individual timelines 822 $('#resultsTable tbody').on('click', 'td.details-control', function () { 823 var tr = $(this).closest('tr'); 824 var row = resultsTable.row(tr); 825 if (row.child.isShown()) { 826 // This row is already open - close it 827 row.child.hide(); 828 tr.removeClass('shown'); 829 $(this).find("i").removeClass("fa-caret-down") 830 $(this).find("i").addClass("fa-caret-right") 831 } else { 832 // Open this row 833 $(this).find("i").removeClass("fa-caret-right") 834 $(this).find("i").addClass("fa-caret-down") 835 row.child(renderTimeline(row.data())).show(); 836 tr.addClass('shown'); 837 } 838 }); 839 // Setup the graphs 840 $.each(campaign.timeline, function (i, event) { 841 if (event.message == "Campaign Created") { 842 return true 843 } 844 var event_date = moment.utc(event.time).local() 845 timeline_series_data.push({ 846 email: event.email, 847 message: event.message, 848 x: event_date.valueOf(), 849 y: 1, 850 marker: { 851 fillColor: statuses[event.message].color 852 } 853 }) 854 }) 855 renderTimelineChart({ 856 data: timeline_series_data 857 }) 858 $.each(email_series_data, function (status, count) { 859 var email_data = [] 860 if (!(status in statusMapping)) { 861 return true 862 } 863 email_data.push({ 864 name: status, 865 y: Math.floor((count / campaign.results.length) * 100), 866 count: count 867 }) 868 email_data.push({ 869 name: '', 870 y: 100 - Math.floor((count / campaign.results.length) * 100) 871 }) 872 var chart = renderPieChart({ 873 elemId: statusMapping[status] + '_chart', 874 title: status, 875 name: status, 876 data: email_data, 877 colors: [statuses[status].color, '#dddddd'] 878 }) 879 }) 880 881 if (use_map) { 882 $("#resultsMapContainer").show() 883 map = new Datamap({ 884 element: document.getElementById("resultsMap"), 885 responsive: true, 886 fills: { 887 defaultFill: "#ffffff", 888 point: "#283F50" 889 }, 890 geographyConfig: { 891 highlightFillColor: "#1abc9c", 892 borderColor: "#283F50" 893 }, 894 bubblesConfig: { 895 borderColor: "#283F50" 896 } 897 }); 898 } 899 updateMap(campaign.results) 900 } 901 }) 902 .error(function () { 903 $("#loading").hide() 904 errorFlash(" Campaign not found!") 905 }) 906 } 907 908 var setRefresh 909 910 function refresh() { 911 if (!doPoll) { 912 return; 913 } 914 $("#refresh_message").show() 915 $("#refresh_btn").hide() 916 poll() 917 clearTimeout(setRefresh) 918 setRefresh = setTimeout(refresh, 60000) 919 }; 920 921 function report_mail(rid, cid) { 922 Swal.fire({ 923 title: "Are you sure?", 924 text: "This result will be flagged as reported (RID: " + rid + ")", 925 type: "question", 926 animation: false, 927 showCancelButton: true, 928 confirmButtonText: "Continue", 929 confirmButtonColor: "#428bca", 930 reverseButtons: true, 931 allowOutsideClick: false, 932 showLoaderOnConfirm: true 933 }).then(function (result) { 934 if (result.value){ 935 api.campaignId.get(cid).success((function(c) { 936 report_url = new URL(c.url) 937 report_url.pathname = '/report' 938 report_url.search = "?rid=" + rid 939 $.ajax({ 940 url: report_url, 941 method: "GET", 942 success: function(data) { 943 refresh(); 944 } 945 }); 946 })); 947 } 948 }) 949 } 950 951 $(document).ready(function () { 952 Highcharts.setOptions({ 953 global: { 954 useUTC: false 955 } 956 }) 957 load(); 958 959 // Start the polling loop 960 setRefresh = setTimeout(refresh, 60000) 961 })