mirror of
https://github.com/gophish/gophish
synced 2024-11-14 16:27:23 +00:00
Adding "Report Email" Support (#1014)
Adds the capability to report phishing campaigns using an email client extension. **Note: Gophish does not currently provide an email client extension out of the box. This is simply a mechanism to let existing email client add-ons send report status information to Gophish, and have that information reflected in the dashboard.**
This commit is contained in:
parent
709e83bade
commit
f21536da7c
17 changed files with 209 additions and 52 deletions
|
@ -39,6 +39,8 @@ func CreatePhishingRouter() http.Handler {
|
|||
router.HandleFunc("/track", PhishTracker)
|
||||
router.HandleFunc("/robots.txt", RobotsHandler)
|
||||
router.HandleFunc("/{path:.*}/track", PhishTracker)
|
||||
router.HandleFunc("/{path:.*}/report", PhishReporter)
|
||||
router.HandleFunc("/report", PhishReporter)
|
||||
router.HandleFunc("/{path:.*}", PhishHandler)
|
||||
return router
|
||||
}
|
||||
|
@ -71,6 +73,29 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
|
|||
http.ServeFile(w, r, "static/images/pixel.png")
|
||||
}
|
||||
|
||||
// PhishReporter tracks emails as they are reported, updating the status for the given Result
|
||||
func PhishReporter(w http.ResponseWriter, r *http.Request) {
|
||||
err, r := setupContext(r)
|
||||
if err != nil {
|
||||
// Log the error if it wasn't something we can safely ignore
|
||||
if err != ErrInvalidRequest && err != ErrCampaignComplete {
|
||||
Logger.Println(err)
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
rs := ctx.Get(r, "result").(models.Result)
|
||||
c := ctx.Get(r, "campaign").(models.Campaign)
|
||||
rj := ctx.Get(r, "details").([]byte)
|
||||
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_REPORTED, Details: string(rj)})
|
||||
|
||||
err = rs.UpdateReported(true)
|
||||
if err != nil {
|
||||
Logger.Println(err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// PhishHandler handles incoming client connections and registers the associated actions performed
|
||||
// (such as clicked link, etc.)
|
||||
func PhishHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -26,6 +26,12 @@ func (s *ControllersSuite) openEmail(rid string) {
|
|||
s.Equal(bytes.Compare(body, expected), 0)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) reportedEmail(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ps.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) openEmail404(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
|
@ -63,6 +69,19 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {
|
|||
s.Equal(result.Status, models.EVENT_OPENED)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestReportedPhishingEmail() {
|
||||
campaign := s.getFirstCampaign()
|
||||
result := campaign.Results[0]
|
||||
s.Equal(result.Status, models.STATUS_SENDING)
|
||||
|
||||
s.reportedEmail(result.RId)
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
result = campaign.Results[0]
|
||||
s.Equal(result.Reported, true)
|
||||
s.Equal(campaign.Events[len(campaign.Events)-1].Message, models.EVENT_REPORTED)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
|
||||
campaign := s.getFirstCampaign()
|
||||
result := campaign.Results[0]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE results ADD COLUMN reported boolean default 0;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE results ADD COLUMN reported boolean default 0;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -33,6 +33,7 @@ type CampaignResults struct {
|
|||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Reported string `json:"reported"`
|
||||
Results []Result `json:"results, omitempty"`
|
||||
Events []Event `json:"timeline,omitempty"`
|
||||
}
|
||||
|
@ -61,6 +62,7 @@ type CampaignStats struct {
|
|||
OpenedEmail int64 `json:"opened"`
|
||||
ClickedLink int64 `json:"clicked"`
|
||||
SubmittedData int64 `json:"submitted_data"`
|
||||
EmailReported int64 `json:"email_reported"`
|
||||
Error int64 `json:"error"`
|
||||
}
|
||||
|
||||
|
@ -194,6 +196,10 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
|
|||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
query.Where("reported=?", true).Count(&s.EmailReported)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
// Every submitted data event implies they clicked the link
|
||||
s.ClickedLink += s.SubmittedData
|
||||
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
|
||||
|
@ -426,6 +432,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
FirstName: t.FirstName,
|
||||
LastName: t.LastName,
|
||||
SendDate: c.LaunchDate,
|
||||
Reported: false,
|
||||
}
|
||||
if c.Status == CAMPAIGN_IN_PROGRESS {
|
||||
r.Status = STATUS_SENDING
|
||||
|
|
|
@ -32,6 +32,7 @@ const (
|
|||
EVENT_OPENED string = "Email Opened"
|
||||
EVENT_CLICKED string = "Clicked Link"
|
||||
EVENT_DATA_SUBMIT string = "Submitted Data"
|
||||
EVENT_REPORTED string = "Email Reported"
|
||||
EVENT_PROXY_REQUEST string = "Proxied request"
|
||||
STATUS_SUCCESS string = "Success"
|
||||
STATUS_QUEUED string = "Queued"
|
||||
|
|
|
@ -38,6 +38,7 @@ type Result struct {
|
|||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
SendDate time.Time `json:"send_date"`
|
||||
Reported bool `json:"reported" sql:"not null"`
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of the result in the database
|
||||
|
@ -45,6 +46,11 @@ func (r *Result) UpdateStatus(s string) error {
|
|||
return db.Table("results").Where("id=?", r.Id).Update("status", s).Error
|
||||
}
|
||||
|
||||
// UpdateReported updates when a user reports a campaign
|
||||
func (r *Result) UpdateReported(s bool) error {
|
||||
return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error
|
||||
}
|
||||
|
||||
// UpdateGeo updates the latitude and longitude of the result in
|
||||
// the database given an IP address
|
||||
func (r *Result) UpdateGeo(addr string) error {
|
||||
|
|
2
static/css/dist/gophish.css
vendored
2
static/css/dist/gophish.css
vendored
File diff suppressed because one or more lines are too long
7
static/css/main.css
vendored
7
static/css/main.css
vendored
|
@ -655,6 +655,11 @@ table.dataTable {
|
|||
color: #f39c12;
|
||||
}
|
||||
|
||||
.color-reported{
|
||||
font-weight: bold;
|
||||
color:#45d6ef;
|
||||
}
|
||||
|
||||
.color-success {
|
||||
color: #f05b4f;
|
||||
}
|
||||
|
@ -666,4 +671,4 @@ table.dataTable {
|
|||
|
||||
#resultsMapContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
2
static/js/dist/app/campaign_results.min.js
vendored
2
static/js/dist/app/campaign_results.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/js/dist/app/campaigns.min.js
vendored
2
static/js/dist/app/campaigns.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/js/dist/app/dashboard.min.js
vendored
2
static/js/dist/app/dashboard.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -27,7 +27,7 @@ var statuses = {
|
|||
"Email Opened": {
|
||||
color: "#f9bf3b",
|
||||
label: "label-warning",
|
||||
icon: "fa-envelope",
|
||||
icon: "fa-envelope-open",
|
||||
point: "ct-point-opened"
|
||||
},
|
||||
"Clicked Link": {
|
||||
|
@ -42,6 +42,13 @@ var statuses = {
|
|||
icon: "fa-exclamation",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
//not a status, but is used for the campaign timeline and user timeline
|
||||
"Email Reported": {
|
||||
color: "#45d6ef",
|
||||
label: "label-info",
|
||||
icon: "fa-bullhorn",
|
||||
point: "ct-point-reported"
|
||||
},
|
||||
"Error": {
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
|
@ -95,6 +102,7 @@ var statusMapping = {
|
|||
"Email Opened": "opened",
|
||||
"Clicked Link": "clicked",
|
||||
"Submitted Data": "submitted_data",
|
||||
"Email Reported": "reported",
|
||||
}
|
||||
|
||||
// This is an underwhelming attempt at an enum
|
||||
|
@ -282,7 +290,8 @@ function renderTimeline(data) {
|
|||
"email": data[4],
|
||||
"position": data[5],
|
||||
"status": data[6],
|
||||
"send_date": data[7]
|
||||
"send_date": data[7],
|
||||
"reported": data[8]
|
||||
}
|
||||
results = '<div class="timeline col-sm-12 well well-lg">' +
|
||||
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
|
||||
|
@ -571,6 +580,9 @@ function poll() {
|
|||
});
|
||||
$.each(campaign.results, function (i, result) {
|
||||
email_series_data[result.status]++;
|
||||
if (result.reported) {
|
||||
email_series_data['Email Reported']++
|
||||
}
|
||||
// Backfill status values
|
||||
var step = progressListing.indexOf(result.status)
|
||||
for (var i = 0; i < step; i++) {
|
||||
|
@ -595,6 +607,7 @@ function poll() {
|
|||
data: email_data
|
||||
})
|
||||
})
|
||||
|
||||
/* Update the datatable */
|
||||
resultsTable = $("#resultsTable").DataTable()
|
||||
resultsTable.rows().every(function (i, tableLoop, rowLoop) {
|
||||
|
@ -603,12 +616,13 @@ function poll() {
|
|||
var rid = rowData[0]
|
||||
$.each(campaign.results, function (j, result) {
|
||||
if (result.id == rid) {
|
||||
rowData[7] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
rowData[7] = result.reported
|
||||
rowData[6] = result.status
|
||||
resultsTable.row(i).data(rowData)
|
||||
if (row.child.isShown()) {
|
||||
$(row.node()).find("i").removeClass("fa-caret-right")
|
||||
$(row.node()).find("i").addClass("fa-caret-down")
|
||||
$(row.node()).find("#caret").removeClass("fa-caret-right")
|
||||
$(row.node()).find("#caret").addClass("fa-caret-down")
|
||||
row.child(renderTimeline(row.data()))
|
||||
}
|
||||
return false
|
||||
|
@ -669,13 +683,24 @@ function load() {
|
|||
"targets": [1]
|
||||
}, {
|
||||
"visible": false,
|
||||
"targets": [0, 7]
|
||||
"targets": [0, 8]
|
||||
},
|
||||
{
|
||||
"render": function (data, type, row) {
|
||||
return createStatusLabel(data, row[7])
|
||||
return createStatusLabel(data, row[8])
|
||||
},
|
||||
"targets": [6]
|
||||
},
|
||||
{
|
||||
className: "text-center",
|
||||
"render": function (reported, type, row) {
|
||||
if (reported) {
|
||||
return "<i class='fa fa-check-circle text-center text-success'></i>"
|
||||
} else {
|
||||
return "<i class='fa fa-times-circle text-center text-danger'></i>"
|
||||
}
|
||||
},
|
||||
"targets": [7]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -688,15 +713,19 @@ function load() {
|
|||
$.each(campaign.results, function (i, result) {
|
||||
resultsTable.row.add([
|
||||
result.id,
|
||||
"<i class=\"fa fa-caret-right\"></i>",
|
||||
"<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
|
||||
escapeHtml(result.first_name) || "",
|
||||
escapeHtml(result.last_name) || "",
|
||||
escapeHtml(result.email) || "",
|
||||
escapeHtml(result.position) || "",
|
||||
result.status,
|
||||
result.reported,
|
||||
moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
])
|
||||
email_series_data[result.status]++;
|
||||
if (result.reported) {
|
||||
email_series_data['Email Reported']++
|
||||
}
|
||||
// Backfill status values
|
||||
var step = progressListing.indexOf(result.status)
|
||||
for (var i = 0; i < step; i++) {
|
||||
|
@ -761,6 +790,7 @@ function load() {
|
|||
colors: [statuses[status].color, '#dddddd']
|
||||
})
|
||||
})
|
||||
|
||||
if (use_map) {
|
||||
$("#resultsMapContainer").show()
|
||||
map = new Datamap({
|
||||
|
|
|
@ -324,7 +324,7 @@ $(document).ready(function () {
|
|||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
|
||||
} else {
|
||||
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
|
||||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported
|
||||
}
|
||||
|
||||
campaignTable.row.add([
|
||||
|
@ -366,4 +366,4 @@ $(document).ready(function () {
|
|||
return 0;
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
var campaigns = []
|
||||
|
||||
// statuses is a helper map to point result statuses to ui classes
|
||||
var statuses = {
|
||||
"Email Sent": {
|
||||
|
@ -29,6 +28,12 @@ var statuses = {
|
|||
icon: "fa-envelope",
|
||||
point: "ct-point-opened"
|
||||
},
|
||||
"Email Reported": {
|
||||
color: "#45d6ef",
|
||||
label: "label-warning",
|
||||
icon: "fa-bullhorne",
|
||||
point: "ct-point-reported"
|
||||
},
|
||||
"Clicked Link": {
|
||||
color: "#F39C12",
|
||||
label: "label-clicked",
|
||||
|
@ -80,6 +85,7 @@ var statuses = {
|
|||
var statsMapping = {
|
||||
"sent": "Email Sent",
|
||||
"opened": "Email Opened",
|
||||
"email_reported": "Email Reported",
|
||||
"clicked": "Clicked Link",
|
||||
"submitted_data": "Submitted Data",
|
||||
}
|
||||
|
@ -107,16 +113,18 @@ function renderPieChart(chartopts) {
|
|||
left = chart.plotLeft + pie.center[0],
|
||||
top = chart.plotTop + pie.center[1];
|
||||
this.innerText = rend.text(chartopts['data'][0].count, left, top).
|
||||
attr({
|
||||
'text-anchor': 'middle',
|
||||
'font-size': '24px',
|
||||
'font-weight': 'bold',
|
||||
'fill': chartopts['colors'][0],
|
||||
'font-family': 'Helvetica,Arial,sans-serif'
|
||||
}).add();
|
||||
attr({
|
||||
'text-anchor': 'middle',
|
||||
'font-size': '16px',
|
||||
'font-weight': 'bold',
|
||||
'fill': chartopts['colors'][0],
|
||||
'font-family': 'Helvetica,Arial,sans-serif'
|
||||
}).add();
|
||||
},
|
||||
render: function () {
|
||||
this.innerText.attr({ text: chartopts['data'][0].count })
|
||||
this.innerText.attr({
|
||||
text: chartopts['data'][0].count
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -190,6 +198,7 @@ function generateStatsPieCharts(campaigns) {
|
|||
data: stats_data,
|
||||
colors: [statuses[status_label].color, "#dddddd"]
|
||||
})
|
||||
|
||||
stats_data = []
|
||||
});
|
||||
}
|
||||
|
@ -289,13 +298,30 @@ $(document).ready(function () {
|
|||
// Create the overview chart data
|
||||
campaignTable = $("#campaignTable").DataTable({
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
},
|
||||
{ className: "color-sent", targets: [2] },
|
||||
{ className: "color-opened", targets: [3] },
|
||||
{ className: "color-clicked", targets: [4] },
|
||||
{ className: "color-success", targets: [5] }],
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
},
|
||||
{
|
||||
className: "color-sent",
|
||||
targets: [2]
|
||||
},
|
||||
{
|
||||
className: "color-opened",
|
||||
targets: [3]
|
||||
},
|
||||
{
|
||||
className: "color-clicked",
|
||||
targets: [4]
|
||||
},
|
||||
{
|
||||
className: "color-success",
|
||||
targets: [5]
|
||||
},
|
||||
{
|
||||
className: "color-reported",
|
||||
targets: [6]
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[1, "desc"]
|
||||
]
|
||||
|
@ -310,7 +336,7 @@ $(document).ready(function () {
|
|||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
|
||||
} else {
|
||||
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
|
||||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported
|
||||
}
|
||||
// Add it to the table
|
||||
campaignTable.row.add([
|
||||
|
@ -320,6 +346,7 @@ $(document).ready(function () {
|
|||
campaign.stats.opened,
|
||||
campaign.stats.clicked,
|
||||
campaign.stats.submitted_data,
|
||||
campaign.stats.email_reported,
|
||||
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
|
||||
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
|
||||
<i class='fa fa-bar-chart'></i>\
|
||||
|
@ -340,4 +367,4 @@ $(document).ready(function () {
|
|||
.error(function () {
|
||||
errorFlash("Error fetching campaigns")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -3,26 +3,35 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-3 col-md-2 sidebar">
|
||||
<ul class="nav nav-sidebar">
|
||||
<li><a href="/">Dashboard</a>
|
||||
<li>
|
||||
<a href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="active"><a href="/campaigns">Campaigns</a>
|
||||
<li class="active">
|
||||
<a href="/campaigns">Campaigns</a>
|
||||
</li>
|
||||
<li><a href="/users">Users & Groups</a>
|
||||
<li>
|
||||
<a href="/users">Users & Groups</a>
|
||||
</li>
|
||||
<li><a href="/templates">Email Templates</a>
|
||||
<li>
|
||||
<a href="/templates">Email Templates</a>
|
||||
</li>
|
||||
<li><a href="/landing_pages">Landing Pages</a>
|
||||
<li>
|
||||
<a href="/landing_pages">Landing Pages</a>
|
||||
</li>
|
||||
<li><a href="/sending_profiles">Sending Profiles</a>
|
||||
<li>
|
||||
<a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li><a href="/settings">Settings</a>
|
||||
<li>
|
||||
<a href="/settings">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr>
|
||||
</li>
|
||||
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
|
||||
<li>
|
||||
<a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
|
||||
</li>
|
||||
<li><a href="/api/">API Documentation</a>
|
||||
<li>
|
||||
<a href="/api/">API Documentation</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -47,8 +56,12 @@
|
|||
<i class="fa fa-caret-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="exportButton">
|
||||
<li><a href="#" onclick="exportAsCSV('results')">Results</a></li>
|
||||
<li><a href="#" onclick="exportAsCSV('events')">Raw Events</a></li>
|
||||
<li>
|
||||
<a href="#" onclick="exportAsCSV('results')">Results</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" onclick="exportAsCSV('events')">Raw Events</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button id="complete_button" type="button" class="btn btn-blue" data-toggle="tooltip" onclick="completeCampaign()">
|
||||
|
@ -73,10 +86,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
|
||||
</div>
|
||||
<div class="row" id="resultsMapContainer">
|
||||
<div class="col-md-6">
|
||||
|
@ -99,6 +115,7 @@
|
|||
<th>Email</th>
|
||||
<th>Position</th>
|
||||
<th>Status</th>
|
||||
<th class="text-center">Reported</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -43,10 +43,13 @@
|
|||
<div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div id="email_reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
|
||||
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Recent Campaigns</h2>
|
||||
|
@ -65,6 +68,7 @@
|
|||
<th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-bullhorn"></i></th>
|
||||
<th class="col-md-1 col-sm-1">Status</th>
|
||||
<th class="col-md-2 col-sm-2 no-sort"></i></th>
|
||||
</tr>
|
||||
|
|
Loading…
Reference in a new issue