Merge pull request #1 from gophish/master

Catching up to the main branch from gophish/master
This commit is contained in:
Justin Gray 2016-01-20 22:27:58 -06:00
commit cf97520ddf
29 changed files with 1291 additions and 948 deletions

View file

@ -3,11 +3,11 @@
gophish
=======
[![Build Status](https://travis-ci.org/gophish/gophish.svg?branch=master)](https://travis-ci.org/gophish/gophish)
[![Build Status](https://travis-ci.org/gophish/gophish.svg?branch=master)](https://travis-ci.org/gophish/gophish) [![GoDoc](https://godoc.org/github.com/gophish/gophish?status.svg)](https://godoc.org/github.com/gophish/gophish)
Open-Source Phishing Toolkit
Gophish is an open-source phishing toolkit designed for businesses and penetration testers. It provides the ability to quickly and easily setup and execute phishing engagements and security awareness training.
[Gophish](https://getgophish.com) is an open-source phishing toolkit designed for businesses and penetration testers. It provides the ability to quickly and easily setup and execute phishing engagements and security awareness training.
###Current Status
**Update 01/12/2016**

View file

@ -1,10 +1,21 @@
{
"admin_url" : "127.0.0.1:3333",
"phish_url" : "0.0.0.0:80",
"admin_server" : {
"listen_url" : "127.0.0.1:3333",
"use_tls" : false,
"cert_path" : "example.crt",
"key_path" : "example.key"
},
"phish_server" : {
"listen_url" : "0.0.0.0:80",
"use_tls" : false,
"cert_path" : "example.crt",
"key_path": "example.key"
},
"smtp" : {
"host" : "smtp.example.com:25",
"user" : "username",
"pass" : "password"
},
"dbpath" : "gophish.db"
}
"db_path" : "gophish.db",
"migrations_path" : "db/migrations/"
}

View file

@ -13,12 +13,29 @@ type SMTPServer struct {
Password string `json:"password"`
}
// AdminServer represents the Admin server configuration details
type AdminServer struct {
ListenURL string `json:"listen_url"`
UseTLS bool `json:"use_tls"`
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
}
// PhishServer represents the Phish server configuration details
type PhishServer struct {
ListenURL string `json:"listen_url"`
UseTLS bool `json:"use_tls"`
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
}
// Config represents the configuration information.
type Config struct {
AdminURL string `json:"admin_url"`
PhishURL string `json:"phish_url"`
SMTP SMTPServer `json:"smtp"`
DBPath string `json:"dbpath"`
AdminConf AdminServer `json:"admin_server"`
PhishConf PhishServer `json:"phish_server"`
SMTPConf SMTPServer `json:"smtp"`
DBPath string `json:"db_path"`
MigrationsPath string `json:"migrations_path"`
}
var Conf Config

View file

@ -10,14 +10,14 @@ import (
"time"
"github.com/PuerkitoBio/goquery"
ctx "github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/jordan-wright/email"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gophish/gophish/worker"
ctx "github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/jordan-wright/email"
)
// Worker is the worker that processes phishing events and updates campaigns.
@ -92,6 +92,7 @@ func API_Campaigns_Id(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(vars["id"], 0, 64)
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
if err != nil {
Logger.Println(err)
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
return
}

View file

@ -9,9 +9,9 @@ import (
"os"
"testing"
"github.com/gorilla/handlers"
"github.com/gophish/gophish/config"
"github.com/gophish/gophish/models"
"github.com/gorilla/handlers"
"github.com/stretchr/testify/suite"
)
@ -26,13 +26,14 @@ var as *httptest.Server = httptest.NewUnstartedServer(handlers.CombinedLoggingHa
func (s *ControllersSuite) SetupSuite() {
config.Conf.DBPath = ":memory:"
config.Conf.MigrationsPath = "../db/migrations/"
err := models.Setup()
if err != nil {
s.T().Fatalf("Failed creating database: %v", err)
}
s.Nil(err)
// Setup the admin server for use in testing
as.Config.Addr = config.Conf.AdminURL
as.Config.Addr = config.Conf.AdminConf.ListenURL
as.Start()
// Get the API key to use for these tests
u, err := models.GetUser(1)

View file

@ -0,0 +1,28 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS "users" ("id" integer primary key autoincrement,"username" varchar(255) NOT NULL UNIQUE,"hash" varchar(255),"api_key" varchar(255) NOT NULL UNIQUE );
CREATE TABLE IF NOT EXISTS "templates" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"subject" varchar(255),"text" varchar(255),"html" varchar(255),"modified_date" datetime );
CREATE TABLE IF NOT EXISTS "targets" ("id" integer primary key autoincrement,"first_name" varchar(255),"last_name" varchar(255),"email" varchar(255),"position" varchar(255) );
CREATE TABLE IF NOT EXISTS "smtp" ("smtp_id" integer primary key autoincrement,"campaign_id" bigint,"host" varchar(255),"username" varchar(255),"from_address" varchar(255) );
CREATE TABLE IF NOT EXISTS "results" ("id" integer primary key autoincrement,"campaign_id" bigint,"user_id" bigint,"r_id" varchar(255),"email" varchar(255),"first_name" varchar(255),"last_name" varchar(255),"status" varchar(255) NOT NULL ,"ip" varchar(255),"latitude" real,"longitude" real );
CREATE TABLE IF NOT EXISTS "pages" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"html" varchar(255),"modified_date" datetime );
CREATE TABLE IF NOT EXISTS "groups" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255),"modified_date" datetime );
CREATE TABLE IF NOT EXISTS "group_targets" ("group_id" bigint,"target_id" bigint );
CREATE TABLE IF NOT EXISTS "events" ("id" integer primary key autoincrement,"campaign_id" bigint,"email" varchar(255),"time" datetime,"message" varchar(255) );
CREATE TABLE IF NOT EXISTS "campaigns" ("id" integer primary key autoincrement,"user_id" bigint,"name" varchar(255) NOT NULL ,"created_date" datetime,"completed_date" datetime,"template_id" bigint,"page_id" bigint,"status" varchar(255),"url" varchar(255) );
CREATE TABLE IF NOT EXISTS "attachments" ("id" integer primary key autoincrement,"template_id" bigint,"content" varchar(255),"type" varchar(255),"name" varchar(255) );
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE "attachments";
DROP TABLE "campaigns";
DROP TABLE "events";
DROP TABLE "group_targets";
DROP TABLE "groups";
DROP TABLE "pages";
DROP TABLE "results";
DROP TABLE "smtp";
DROP TABLE "targets";
DROP TABLE "templates";
DROP TABLE "users";

View file

@ -30,11 +30,12 @@ import (
"log"
"net/http"
"os"
"sync"
"github.com/gorilla/handlers"
"github.com/gophish/gophish/config"
"github.com/gophish/gophish/controllers"
"github.com/gophish/gophish/models"
"github.com/gorilla/handlers"
)
var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile)
@ -45,9 +46,31 @@ func main() {
if err != nil {
fmt.Println(err)
}
wg := &sync.WaitGroup{}
wg.Add(1)
// Start the web servers
Logger.Printf("Admin server started at http://%s\n", config.Conf.AdminURL)
go http.ListenAndServe(config.Conf.AdminURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter()))
Logger.Printf("Phishing server started at http://%s\n", config.Conf.PhishURL)
http.ListenAndServe(config.Conf.PhishURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter()))
go func() {
defer wg.Done()
if config.Conf.AdminConf.UseTLS { // use TLS for Admin web server if available
Logger.Printf("Starting admin server at https://%s\n", config.Conf.AdminConf.ListenURL)
Logger.Fatal(http.ListenAndServeTLS(config.Conf.AdminConf.ListenURL, config.Conf.AdminConf.CertPath, config.Conf.AdminConf.KeyPath,
handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter())))
} else {
Logger.Printf("Starting admin server at http://%s\n", config.Conf.AdminConf.ListenURL)
Logger.Fatal(http.ListenAndServe(config.Conf.AdminConf.ListenURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreateAdminRouter())))
}
}()
wg.Add(1)
go func() {
defer wg.Done()
if config.Conf.PhishConf.UseTLS { // use TLS for Phish web server if available
Logger.Printf("Starting phishing server at https://%s\n", config.Conf.PhishConf.ListenURL)
Logger.Fatal(http.ListenAndServeTLS(config.Conf.PhishConf.ListenURL, config.Conf.PhishConf.CertPath, config.Conf.PhishConf.KeyPath,
handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter())))
} else {
Logger.Printf("Starting phishing server at http://%s\n", config.Conf.PhishConf.ListenURL)
Logger.Fatal(http.ListenAndServe(config.Conf.PhishConf.ListenURL, handlers.CombinedLoggingHandler(os.Stdout, controllers.CreatePhishingRouter())))
}
}()
wg.Wait()
}

View file

@ -117,21 +117,28 @@ func GetCampaign(id int64, uid int64) (Campaign, error) {
c := Campaign{}
err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error
if err != nil {
Logger.Printf("%s: campaign not found\n", err)
return c, err
}
err = db.Model(&c).Related(&c.Results).Error
if err != nil {
Logger.Printf("%s: results not found for campaign\n", err)
return c, err
}
err = db.Model(&c).Related(&c.Events).Error
if err != nil {
Logger.Printf("%s: events not found for campaign\n", err)
return c, err
}
err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error
if err != nil {
Logger.Printf("%s: template not found for campaign\n", err)
return c, err
}
err = db.Table("pages").Where("id=?", c.PageId).Find(&c.Page).Error
if err != nil {
Logger.Printf("%s: page not found for campaign\n", err)
}
return c, err
}
@ -207,6 +214,7 @@ func PostCampaign(c *Campaign, uid int64) error {
//DeleteCampaign deletes the specified campaign
func DeleteCampaign(id int64) error {
Logger.Printf("Deleting campaign %d\n", id)
// Delete all the campaign results
err := db.Where("campaign_id=?", id).Delete(&Result{}).Error
if err != nil {

View file

@ -8,6 +8,8 @@ import (
"log"
"os"
"bitbucket.org/liamstask/goose/lib/goose"
"github.com/gophish/gophish/config"
"github.com/jinzhu/gorm"
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
@ -62,6 +64,24 @@ func Setup() error {
if _, err = os.Stat(config.Conf.DBPath); err != nil || config.Conf.DBPath == ":memory:" {
create_db = true
}
// Setup the goose configuration
migrateConf := &goose.DBConf{
MigrationsDir: config.Conf.MigrationsPath,
Env: "production",
Driver: goose.DBDriver{
Name: "sqlite3",
OpenStr: config.Conf.DBPath,
Import: "github.com/mattn/go-sqlite3",
Dialect: &goose.Sqlite3Dialect{},
},
}
// Get the latest possible migration
latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
if err != nil {
Logger.Println(err)
return err
}
// Open our database connection
db, err = gorm.Open("sqlite3", config.Conf.DBPath)
db.LogMode(false)
db.SetLogger(Logger)
@ -69,20 +89,14 @@ func Setup() error {
Logger.Println(err)
return err
}
//If the file already exists, delete it and recreate it
// Migrate up to the latest version
err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, db.DB())
if err != nil {
Logger.Println(err)
return err
}
//If the database didn't exist, we need to create the admin user
if create_db {
Logger.Printf("Database not found... creating db at %s\n", config.Conf.DBPath)
db.CreateTable(User{})
db.CreateTable(Target{})
db.CreateTable(Result{})
db.CreateTable(Group{})
db.CreateTable(GroupTarget{})
db.CreateTable(Template{})
db.CreateTable(Attachment{})
db.CreateTable(Page{})
db.CreateTable(SMTP{})
db.CreateTable(Event{})
db.CreateTable(Campaign{})
//Create the default user
initUser := User{
Username: "admin",
@ -92,6 +106,7 @@ func Setup() error {
err = db.Save(&initUser).Error
if err != nil {
Logger.Println(err)
return err
}
}
return nil

View file

@ -16,6 +16,7 @@ var _ = check.Suite(&ModelsSuite{})
func (s *ModelsSuite) SetUpSuite(c *check.C) {
config.Conf.DBPath = ":memory:"
config.Conf.MigrationsPath = "../db/migrations/"
err := Setup()
if err != nil {
c.Fatalf("Failed creating database: %v", err)

30
static/css/main.css vendored
View file

@ -341,3 +341,33 @@
float:none !important;
}
}
/* Table Styling */
.modal-content .dataTable tbody td {
font-size: 16px;/* Smaller font on modal tables */
}
.dataTables_info{
font-size: 15px;
}
/* Sort Icons */
table.dataTable thead .sorting:after, table.dataTable thead .sorting_asc:after, table.dataTable thead .sorting_desc:after {
font-family: 'FontAwesome' !important;
position: relative !important;
display: initial !important;
top: initial!important;
right: initial!important;
left: 6px;
color: #1abc9c;
}
table.dataTable thead .sorting:after{
content: "\f0dc" !important;
color: initial;
}
table.dataTable thead .sorting_asc:after {
content: "\f0de" !important;
opacity: .8 !important;
}
table.dataTable thead .sorting_desc:after {
content: "\f0dd" !important;
opacity: .8 !important;
}

View file

@ -2,254 +2,287 @@ var map = null
// statuses is a helper map to point result statuses to ui classes
var statuses = {
"Email Sent" : {
"Email Sent": {
slice: "ct-slice-donut-sent",
legend: "ct-legend-sent",
label: "label-success"
},
"Email Opened" : {
"Email Opened": {
slice: "ct-slice-donut-opened",
legend: "ct-legend-opened",
label: "label-warning"
},
"Clicked Link" : {
"Clicked Link": {
slice: "ct-slice-donut-clicked",
legend: "ct-legend-clicked",
label: "label-danger"
},
"Success" : {
"Success": {
slice: "ct-slice-donut-clicked",
legend: "ct-legend-clicked",
label: "label-danger"
},
"Error" : {
"Error": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
label: "label-default"
},
"Unknown" : {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
label: "label-default"
"Unknown": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
label: "label-default"
}
}
var campaign = {}
function dismiss(){
function dismiss() {
$("#modal\\.flashes").empty()
$("#modal").modal('hide')
$("#resultsTable").dataTable().DataTable().clear().draw()
}
// Deletes a campaign after prompting the user
function deleteCampaign(){
if (confirm("Are you sure you want to delete: " + campaign.name + "?")){
function deleteCampaign() {
if (confirm("Are you sure you want to delete: " + campaign.name + "?")) {
api.campaignId.delete(campaign.id)
.success(function(msg){
console.log(msg)
})
.error(function(e){
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
.success(function(msg) {
location.href = '/campaigns'
})
.error(function(e) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
})
})
}
}
$(document).ready(function(){
// Exports campaign results as a CSV file
function exportAsCSV() {
exportHTML = $("#exportButton").html()
$("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>')
var csvString = Papa.unparse(campaign.results, {})
var csvData = new Blob([csvString], {
type: 'text/csv;charset=utf-8;'
});
if (navigator.msSaveBlob) {
navigator.msSaveBlob(csvData, 'results.csv');
} else {
var csvURL = window.URL.createObjectURL(csvData);
var dlLink = document.createElement('a');
dlLink.href = csvURL;
dlLink.setAttribute('download', 'results.csv');
dlLink.click();
}
$("#exportButton").html(exportHTML)
}
$(document).ready(function() {
campaign.id = window.location.pathname.split('/').slice(-1)[0]
api.campaignId.get(campaign.id)
.success(function(c){
campaign = c
if (campaign){
// Set the title
$("#page-title").text("Results for " + c.name)
// Setup tooltips
$('[data-toggle="tooltip"]').tooltip()
// Setup our graphs
var timeline_data = {series:[{
name: "Events",
data: []
}]}
var email_data = {series:[]}
var email_legend = {}
var email_series_data = {}
var timeline_opts = {
axisX: {
showGrid: false,
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value){
return moment(value).format('MMMM Do YYYY h:mm')
}
},
axisY: {
type: Chartist.FixedScaleAxis,
ticks: [0, 1, 2],
low: 0,
showLabel: false
},
showArea: false,
plugins: []
}
var email_opts = {
donut : true,
donutWidth: 40,
chartPadding: 0,
showLabel: false
}
// Setup the results table
resultsTable = $("#resultsTable").DataTable();
$.each(campaign.results, function(i, result){
label = statuses[result.status].label || "label-default";
resultsTable.row.add([
result.first_name || "",
result.last_name || "",
result.email || "",
result.position || "",
"<span class=\"label " + label + "\">" + result.status + "</span>"
]).draw()
if (!email_series_data[result.status]){
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++;
.success(function(c) {
campaign = c
if (campaign) {
// Set the title
$("#page-title").text("Results for " + c.name)
// Setup tooltips
$('[data-toggle="tooltip"]').tooltip()
// Setup our graphs
var timeline_data = {
series: [{
name: "Events",
data: []
}]
}
})
// Setup the graphs
$.each(campaign.timeline, function(i, event){
timeline_data.series[0].data.push({meta : i, x: new Date(event.time), y:1})
})
$.each(email_series_data, function(status, count){
email_data.series.push({meta: status, value: count})
})
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
// Setup the overview chart listeners
$chart = $("#timeline_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-point', function() {
var $point = $(this)
value = $point.attr('ct:value')
cidx = $point.attr('ct:meta')
html = "Event: " + campaign.timeline[cidx].message
if (campaign.timeline[cidx].email) {
html += '<br>' + "Email: " + campaign.timeline[cidx].email
var email_data = {
series: []
}
$toolTip.html(html).show()
});
$chart.on('mouseleave', '.ct-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
});
});
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
email_chart.on('draw', function(data){
// We don't want to create the legend twice
if (!email_legend[data.meta]) {
console.log(data.meta)
$("#email_chart_legend").append('<li><span class="' + statuses[data.meta].legend + '"></span>' + data.meta + '</li>')
email_legend[data.meta] = true
}
data.element.addClass(statuses[data.meta].slice)
})
// Setup the average chart listeners
$piechart = $("#email_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString()).show();
});
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
});
});
$("#loading").hide()
$("#campaignResults").show()
map = new Datamap({
element: document.getElementById("resultsMap"),
responsive: true,
fills: {
defaultFill: "#ffffff",
point: "#283F50"
},
geographyConfig: {
highlightFillColor : "#1abc9c",
borderColor:"#283F50"
},
bubblesConfig: {
borderColor: "#283F50"
}
});
bubbles = []
$.each(campaign.results, function(i, result){
// Check that it wasn't an internal IP
if (result.latitude == 0 && result.longitude == 0) { return true; }
newIP = true
$.each(bubbles, function(i, bubble){
if (bubble.ip == result.ip){
bubbles[i].radius += 1
newIP = false
return false
}
})
if (newIP){
console.log("Adding bubble at: ")
console.log({
latitude : result.latitude,
longitude: result.longitude,
name : result.ip,
fillKey: "point"
})
bubbles.push({
latitude : result.latitude,
longitude: result.longitude,
name : result.ip,
fillKey: "point",
radius: 2
})
}
})
map.bubbles(bubbles)
}
// Load up the map data (only once!)
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if ($(e.target).attr('href') == "#overview"){
if (!map){
map = new Datamap({
element: document.getElementById("resultsMap"),
responsive: true,
fills: {
defaultFill: "#ffffff"
},
geographyConfig: {
highlightFillColor : "#1abc9c",
borderColor:"#283F50"
var email_legend = {}
var email_series_data = {}
var timeline_opts = {
axisX: {
showGrid: false,
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value) {
return moment(value).format('MMMM Do YYYY h:mm')
}
});
},
axisY: {
type: Chartist.FixedScaleAxis,
ticks: [0, 1, 2],
low: 0,
showLabel: false
},
showArea: false,
plugins: []
}
var email_opts = {
donut: true,
donutWidth: 40,
chartPadding: 0,
showLabel: false
}
// Setup the results table
resultsTable = $("#resultsTable").DataTable();
$.each(campaign.results, function(i, result) {
label = statuses[result.status].label || "label-default";
resultsTable.row.add([
result.first_name || "",
result.last_name || "",
result.email || "",
result.position || "",
"<span class=\"label " + label + "\">" + result.status + "</span>"
]).draw()
if (!email_series_data[result.status]) {
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++;
}
})
// Setup the graphs
$.each(campaign.timeline, function(i, event) {
timeline_data.series[0].data.push({
meta: i,
x: new Date(event.time),
y: 1
})
})
$.each(email_series_data, function(status, count) {
email_data.series.push({
meta: status,
value: count
})
})
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
// Setup the overview chart listeners
$chart = $("#timeline_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-point', function() {
var $point = $(this)
value = $point.attr('ct:value')
cidx = $point.attr('ct:meta')
html = "Event: " + campaign.timeline[cidx].message
if (campaign.timeline[cidx].email) {
html += '<br>' + "Email: " + campaign.timeline[cidx].email
}
$toolTip.html(html).show()
});
$chart.on('mouseleave', '.ct-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
});
});
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
email_chart.on('draw', function(data) {
// We don't want to create the legend twice
if (!email_legend[data.meta]) {
console.log(data.meta)
$("#email_chart_legend").append('<li><span class="' + statuses[data.meta].legend + '"></span>' + data.meta + '</li>')
email_legend[data.meta] = true
}
data.element.addClass(statuses[data.meta].slice)
})
// Setup the average chart listeners
$piechart = $("#email_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString()).show();
});
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
});
});
$("#loading").hide()
$("#campaignResults").show()
map = new Datamap({
element: document.getElementById("resultsMap"),
responsive: true,
fills: {
defaultFill: "#ffffff",
point: "#283F50"
},
geographyConfig: {
highlightFillColor: "#1abc9c",
borderColor: "#283F50"
},
bubblesConfig: {
borderColor: "#283F50"
}
});
bubbles = []
$.each(campaign.results, function(i, result) {
// Check that it wasn't an internal IP
if (result.latitude == 0 && result.longitude == 0) {
return true;
}
newIP = true
$.each(bubbles, function(i, bubble) {
if (bubble.ip == result.ip) {
bubbles[i].radius += 1
newIP = false
return false
}
})
if (newIP) {
console.log("Adding bubble at: ")
console.log({
latitude: result.latitude,
longitude: result.longitude,
name: result.ip,
fillKey: "point"
})
bubbles.push({
latitude: result.latitude,
longitude: result.longitude,
name: result.ip,
fillKey: "point",
radius: 2
})
}
})
map.bubbles(bubbles)
}
// Load up the map data (only once!)
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
if ($(e.target).attr('href') == "#overview") {
if (!map) {
map = new Datamap({
element: document.getElementById("resultsMap"),
responsive: true,
fills: {
defaultFill: "#ffffff"
},
geographyConfig: {
highlightFillColor: "#1abc9c",
borderColor: "#283F50"
}
});
}
}
})
})
.error(function() {
$("#loading").hide()
errorFlash(" Campaign not found!")
})
})
.error(function(){
$("#loading").hide()
errorFlash(" Campaign not found!")
})
})

View file

@ -1,212 +1,253 @@
// labels is a map of campaign statuses to
// CSS classes
var labels = {
"In progress" : "label-primary",
"Queued" : "label-info",
"Completed" : "label-success",
"Emails Sent" : "label-success",
"Error" : "label-danger"
"In progress": "label-primary",
"Queued": "label-info",
"Completed": "label-success",
"Emails Sent": "label-success",
"Error": "label-danger"
}
var campaigns = []
// Save attempts to POST to /campaigns/
function save(){
groups = []
$.each($("#groupTable").DataTable().rows().data(), function(i, group){
groups.push({name: group[0]})
})
console.log(groups)
var campaign = {
name: $("#name").val(),
template:{
name: $("#template").val()
},
url: $("#url").val(),
page: {
name: $("#page").val()
},
smtp: {
from_address: $("input[name=from]").val(),
host: $("input[name=host]").val(),
username: $("input[name=username]").val(),
password: $("input[name=password]").val(),
},
groups: groups
function launch() {
if (!confirm("This will launch the campaign. Are you sure?")) {
return false;
}
// Submit the campaign
groups = []
$.each($("#groupTable").DataTable().rows().data(), function(i, group) {
groups.push({
name: group[0]
})
})
var campaign = {
name: $("#name").val(),
template: {
name: $("#template").val()
},
url: $("#url").val(),
page: {
name: $("#page").val()
},
smtp: {
from_address: $("input[name=from]").val(),
host: $("input[name=host]").val(),
username: $("input[name=username]").val(),
password: $("input[name=password]").val(),
},
groups: groups
}
// Submit the campaign
api.campaigns.post(campaign)
.success(function(data){
successFlash("Campaign successfully launched!")
window.location = "/campaigns/" + campaign.id.toString()
})
.error(function(data){
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
.success(function(data) {
successFlash("Campaign successfully launched!")
window.location = "/campaigns/" + data.id.toString()
})
.error(function(data) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
})
})
}
function dismiss(){
function dismiss() {
$("#modal\\.flashes").empty()
$("#modal").modal('hide')
$("#groupTable").dataTable().DataTable().clear().draw()
}
function edit(campaign){
function deleteCampaign(idx) {
if (confirm("Delete " + campaigns[idx].name + "?")) {
api.campaignId.delete(campaigns[idx].id)
.success(function(data) {
successFlash(data.message)
location.reload()
})
}
}
function edit(campaign) {
// Clear the bloodhound instance
group_bh.clear();
template_bh.clear();
page_bh.clear();
if (campaign == "new") {
api.groups.get()
.success(function(groups){
if (groups.length == 0){
modalError("No groups found!")
return false;
}
else {
group_bh.add(groups)
}
})
api.templates.get()
.success(function(templates){
if (templates.length == 0){
modalError("No templates found!")
return false
}
else {
template_bh.add(templates)
}
})
api.pages.get()
.success(function(pages){
if (pages.length == 0){
modalError("No pages found!")
return false
}
else {
page_bh.add(pages)
}
})
.success(function(groups) {
if (groups.length == 0) {
modalError("No groups found!")
return false;
} else {
group_bh.add(groups)
}
})
api.templates.get()
.success(function(templates) {
if (templates.length == 0) {
modalError("No templates found!")
return false
} else {
template_bh.add(templates)
}
})
api.pages.get()
.success(function(pages) {
if (pages.length == 0) {
modalError("No pages found!")
return false
} else {
page_bh.add(pages)
}
})
}
}
$(document).ready(function(){
$(document).ready(function() {
api.campaigns.get()
.success(function(campaigns){
$("#loading").hide()
if (campaigns.length > 0){
$("#campaignTable").show()
campaignTable = $("#campaignTable").DataTable();
$.each(campaigns, function(i, campaign){
label = labels[campaign.status] || "label-default";
campaignTable.row.add([
campaign.name,
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "'>\
.success(function(cs) {
campaigns = cs
campaignTable = $("#campaignTable").DataTable({
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
$("#loading").hide()
if (campaigns.length > 0) {
$("#campaignTable").show()
campaignTable = $("#campaignTable").DataTable();
$.each(campaigns, function(i, campaign) {
label = labels[campaign.status] || "label-default";
campaignTable.row.add([
campaign.name,
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
"<span class=\"label " + label + "\">" + 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>\
</a>\
<button class='btn btn-danger' onclick='alert(\"test\")'>\
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function(){
$("#loading").hide()
errorFlash("Error fetching campaigns")
})
$("#groupForm").submit(function(){
groupTable.row.add([
$("#groupSelect").val(),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
$("#groupTable").on("click", "span>i.fa-trash-o", function(){
groupTable.row( $(this).parents('tr') )
.remove()
.draw();
]).draw()
$('[data-toggle="tooltip"]').tooltip()
})
} else {
$("#emptyMessage").show()
}
})
return false;
.error(function() {
$("#loading").hide()
errorFlash("Error fetching campaigns")
})
$("#groupForm").submit(function() {
groupTable.row.add([
$("#groupSelect").val(),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
$("#groupTable").on("click", "span>i.fa-trash-o", function() {
groupTable.row($(this).parents('tr'))
.remove()
.draw();
})
return false;
})
// Create the group typeahead objects
groupTable = $("#groupTable").DataTable({
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
})
// Create the group typeahead objects
groupTable = $("#groupTable").DataTable()
group_bh = new Bloodhound({
datumTokenizer: function(g) { return Bloodhound.tokenizers.whitespace(g.name) },
datumTokenizer: function(g) {
return Bloodhound.tokenizers.whitespace(g.name)
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: []
})
group_bh.initialize()
$("#groupSelect.typeahead.form-control").typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
name: "groups",
source: group_bh,
templates: {
empty: function(data) {return '<div class="tt-suggestion">No groups matched that query</div>' },
suggestion: function(data){ return '<div>' + data.name + '</div>' }
}
})
.bind('typeahead:select', function(ev, group){
$("#groupSelect").typeahead('val', group.name)
})
.bind('typeahead:autocomplete', function(ev, group){
$("#groupSelect").typeahead('val', group.name)
});
hint: true,
highlight: true,
minLength: 1
}, {
name: "groups",
source: group_bh,
templates: {
empty: function(data) {
return '<div class="tt-suggestion">No groups matched that query</div>'
},
suggestion: function(data) {
return '<div>' + data.name + '</div>'
}
}
})
.bind('typeahead:select', function(ev, group) {
$("#groupSelect").typeahead('val', group.name)
})
.bind('typeahead:autocomplete', function(ev, group) {
$("#groupSelect").typeahead('val', group.name)
});
// Create the template typeahead objects
template_bh = new Bloodhound({
datumTokenizer: function(t) { return Bloodhound.tokenizers.whitespace(t.name) },
datumTokenizer: function(t) {
return Bloodhound.tokenizers.whitespace(t.name)
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: []
})
template_bh.initialize()
$("#template.typeahead.form-control").typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
name: "templates",
source: template_bh,
templates: {
empty: function(data) {return '<div class="tt-suggestion">No templates matched that query</div>' },
suggestion: function(data){ return '<div>' + data.name + '</div>' }
}
})
.bind('typeahead:select', function(ev, template){
$("#template").typeahead('val', template.name)
})
.bind('typeahead:autocomplete', function(ev, template){
$("#template").typeahead('val', template.name)
});
hint: true,
highlight: true,
minLength: 1
}, {
name: "templates",
source: template_bh,
templates: {
empty: function(data) {
return '<div class="tt-suggestion">No templates matched that query</div>'
},
suggestion: function(data) {
return '<div>' + data.name + '</div>'
}
}
})
.bind('typeahead:select', function(ev, template) {
$("#template").typeahead('val', template.name)
})
.bind('typeahead:autocomplete', function(ev, template) {
$("#template").typeahead('val', template.name)
});
// Create the landing page typeahead objects
page_bh = new Bloodhound({
datumTokenizer: function(p) { return Bloodhound.tokenizers.whitespace(p.name) },
datumTokenizer: function(p) {
return Bloodhound.tokenizers.whitespace(p.name)
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: []
})
page_bh.initialize()
$("#page.typeahead.form-control").typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
name: "pages",
source: page_bh,
templates: {
empty: function(data) {return '<div class="tt-suggestion">No pages matched that query</div>' },
suggestion: function(data){ return '<div>' + data.name + '</div>' }
}
})
.bind('typeahead:select', function(ev, page){
$("#page").typeahead('val', page.name)
})
.bind('typeahead:autocomplete', function(ev, page){
$("#page").typeahead('val', page.name)
});
hint: true,
highlight: true,
minLength: 1
}, {
name: "pages",
source: page_bh,
templates: {
empty: function(data) {
return '<div class="tt-suggestion">No pages matched that query</div>'
},
suggestion: function(data) {
return '<div>' + data.name + '</div>'
}
}
})
.bind('typeahead:select', function(ev, page) {
$("#page").typeahead('val', page.name)
})
.bind('typeahead:autocomplete', function(ev, page) {
$("#page").typeahead('val', page.name)
});
})

View file

@ -1,129 +1,159 @@
var campaigns = []
// labels is a map of campaign statuses to
// CSS classes
// labels is a map of campaign statuses to
// CSS classes
var labels = {
"In progress" : "label-primary",
"Queued" : "label-info",
"Completed" : "label-success",
"Emails Sent" : "label-success",
"Error" : "label-danger"
"In progress": "label-primary",
"Queued": "label-info",
"Completed": "label-success",
"Emails Sent": "label-success",
"Error": "label-danger"
}
$(document).ready(function(){
function deleteCampaign(idx) {
if (confirm("Delete " + campaigns[idx].name + "?")) {
api.campaignId.delete(campaigns[idx].id)
.success(function(data) {
successFlash(data.message)
location.reload()
})
}
}
$(document).ready(function() {
api.campaigns.get()
.success(function(cs){
$("#loading").hide()
campaigns = cs
if (campaigns.length > 0){
$("#dashboard").show()
// Create the overview chart data
var overview_data = {labels:[],series:[[]]}
var average_data = {series:[]}
var overview_opts = {
axisX: {
showGrid: false
},
showArea: true,
plugins: []
}
var average_opts = {
donut : true,
donutWidth: 40,
chartPadding: 0,
showLabel: false
}
var average = 0
campaignTable = $("#campaignTable").DataTable();
$.each(campaigns, function(i, campaign){
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY h:mm:ss a')
var label = labels[campaign.status] || "label-default";
// Add it to the table
campaignTable.row.add([
campaign.name,
campaign_date,
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "'>\
.success(function(cs) {
$("#loading").hide()
campaigns = cs
if (campaigns.length > 0) {
$("#dashboard").show()
// Create the overview chart data
var overview_data = {
labels: [],
series: [
[]
]
}
var average_data = {
series: []
}
var overview_opts = {
axisX: {
showGrid: false
},
showArea: true,
plugins: []
}
var average_opts = {
donut: true,
donutWidth: 40,
chartPadding: 0,
showLabel: false
}
var average = 0
campaignTable = $("#campaignTable").DataTable({
columnDefs: [
{ orderable: false, targets: "no-sort" }
]
});
$.each(campaigns, function(i, campaign) {
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY h:mm:ss a')
var label = labels[campaign.status] || "label-default";
// Add it to the table
campaignTable.row.add([
campaign.name,
campaign_date,
"<span class=\"label " + label + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='right' title='View Results'>\
<i class='fa fa-bar-chart'></i>\
</a>\
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")'>\
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='right' title='Delete Campaign'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
// Add it to the chart data
campaign.y = 0
$.each(campaign.results, function(j, result){
if (result.status == "Success"){
campaign.y++;
}
]).draw()
// Add it to the chart data
campaign.y = 0
$.each(campaign.results, function(j, result) {
if (result.status == "Success") {
campaign.y++;
}
})
campaign.y = Math.floor((campaign.y / campaign.results.length) * 100)
average += campaign.y
// Add the data to the overview chart
overview_data.labels.push(campaign_date)
overview_data.series[0].push({
meta: i,
value: campaign.y
})
})
campaign.y = Math.floor((campaign.y / campaign.results.length) * 100)
average += campaign.y
// Add the data to the overview chart
overview_data.labels.push(campaign_date)
overview_data.series[0].push({meta : i, value: campaign.y})
})
average = Math.floor(average / campaigns.length);
average_data.series.push({meta: "Unsuccessful Phishes", value: 100 - average})
average_data.series.push({meta: "Successful Phishes", value: average})
// Build the charts
var average_chart = new Chartist.Pie("#average_chart", average_data, average_opts)
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
// Setup the average chart listeners
$piechart = $("#average_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
average = Math.floor(average / campaigns.length);
average_data.series.push({
meta: "Unsuccessful Phishes",
value: 100 - average
})
average_data.series.push({
meta: "Successful Phishes",
value: average
})
// Build the charts
var average_chart = new Chartist.Pie("#average_chart", average_data, average_opts)
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
// Setup the average chart listeners
$piechart = $("#average_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
});
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
});
});
// Setup the overview chart listeners
$chart = $("#overview_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-point', function() {
var $point = $(this)
value = $point.attr('ct:value') || 0
cidx = $point.attr('ct:meta')
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
});
$chart.on('mouseleave', '.ct-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
});
$("#overview_chart").on("click", ".ct-point", function(e) {
var $cidx = $(this).attr('ct:meta');
window.location.href = "/campaigns/" + campaigns[cidx].id
});
} else {
$("#emptyMessage").show()
}
})
.error(function(){
errorFlash("Error fetching campaigns")
})
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
});
});
// Setup the overview chart listeners
$chart = $("#overview_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-point', function() {
var $point = $(this)
value = $point.attr('ct:value') || 0
cidx = $point.attr('ct:meta')
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
});
$chart.on('mouseleave', '.ct-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
});
});
$("#overview_chart").on("click", ".ct-point", function(e) {
var $cidx = $(this).attr('ct:meta');
window.location.href = "/campaigns/" + campaigns[cidx].id
});
} else {
$("#emptyMessage").show()
}
})
.error(function() {
errorFlash("Error fetching campaigns")
})
})

View file

@ -4,72 +4,75 @@
Author: Jordan Wright <github.com/jordan-wright>
*/
var pages = []
// Save attempts to POST to /templates/
function save(idx){
function save(idx) {
var page = {}
page.name = $("#name").val()
page.html = CKEDITOR.instances["html_editor"].getData();
if (idx != -1){
if (idx != -1) {
page.id = pages[idx].id
api.pageId.put(page)
.success(function(data){
successFlash("Page edited successfully!")
load()
dismiss()
})
.success(function(data) {
successFlash("Page edited successfully!")
load()
dismiss()
})
} else {
// Submit the page
api.pages.post(page)
.success(function(data){
successFlash("Page added successfully!")
load()
dismiss()
})
.error(function(data){
modalError(data.responseJSON.message)
})
.success(function(data) {
successFlash("Page added successfully!")
load()
dismiss()
})
.error(function(data) {
modalError(data.responseJSON.message)
})
}
}
function dismiss(){
function dismiss() {
$("#modal\\.flashes").empty()
$("#name").val("")
$("#html_editor").val("")
$("#newLandingPageModal").modal('hide')
}
function deletePage(idx){
if (confirm("Delete " + pages[idx].name + "?")){
function deletePage(idx) {
if (confirm("Delete " + pages[idx].name + "?")) {
api.pageId.delete(pages[idx].id)
.success(function(data){
successFlash(data.message)
load()
})
.success(function(data) {
successFlash(data.message)
load()
})
}
}
function importSite(){
function importSite() {
url = $("#url").val()
if (!url){
if (!url) {
modalError("No URL Specified!")
} else {
api.clone_site({
url: url,
include_resources: false
})
.success(function(data){
console.log($("#html_editor"))
$("#html_editor").val(data.html)
$("#importSiteModal").modal("hide")
})
.error(function(data){
modalError(data.responseJSON.message)
})
url: url,
include_resources: false
})
.success(function(data) {
console.log($("#html_editor"))
$("#html_editor").val(data.html)
$("#importSiteModal").modal("hide")
})
.error(function(data) {
modalError(data.responseJSON.message)
})
}
}
function edit(idx){
$("#modalSubmit").unbind('click').click(function(){save(idx)})
function edit(idx) {
$("#modalSubmit").unbind('click').click(function() {
save(idx)
})
$("#html_editor").ckeditor()
var page = {}
if (idx != -1) {
@ -79,82 +82,86 @@ function edit(idx){
}
}
function load(){
/*
load() - Loads the current pages using the API
*/
function load() {
/*
load() - Loads the current pages using the API
*/
$("#pagesTable").hide()
$("#emptyMessage").hide()
$("#loading").show()
api.pages.get()
.success(function(ps){
pages = ps
$("#loading").hide()
if (pages.length > 0){
$("#pagesTable").show()
pagesTable = $("#pagesTable").DataTable();
pagesTable.clear()
$.each(pages, function(i, page){
pagesTable.row.add([
page.name,
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#newLandingPageModal' onclick='edit(" + i + ")'>\
.success(function(ps) {
pages = ps
$("#loading").hide()
if (pages.length > 0) {
$("#pagesTable").show()
pagesTable = $("#pagesTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
pagesTable.clear()
$.each(pages, function(i, page) {
pagesTable.row.add([
page.name,
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#newLandingPageModal' onclick='edit(" + i + ")'>\
<i class='fa fa-pencil'></i>\
</button>\
<button class='btn btn-danger' onclick='deletePage(" + i + ")'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function(){
$("#loading").hide()
errorFlash("Error fetching pages")
})
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function() {
$("#loading").hide()
errorFlash("Error fetching pages")
})
}
$(document).ready(function(){
$(document).ready(function() {
// Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function( event ) {
$(this).removeClass( 'fv-modal-stack' );
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) - 1 );
$('.modal').on('hidden.bs.modal', function(event) {
$(this).removeClass('fv-modal-stack');
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
});
$( '.modal' ).on( 'shown.bs.modal', function ( event ) {
$('.modal').on('shown.bs.modal', function(event) {
// Keep track of the number of open modals
if ( typeof( $('body').data( 'fv_open_modals' ) ) == 'undefined' )
{
$('body').data( 'fv_open_modals', 0 );
if (typeof($('body').data('fv_open_modals')) == 'undefined') {
$('body').data('fv_open_modals', 0);
}
// if the z-index of this modal has been set, ignore.
if ( $(this).hasClass( 'fv-modal-stack' ) )
{
if ($(this).hasClass('fv-modal-stack')) {
return;
}
$(this).addClass( 'fv-modal-stack' );
// Increment the number of open modals
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) + 1 );
// Setup the appropriate z-index
$(this).css('z-index', 1040 + (10 * $('body').data( 'fv_open_modals' )));
$( '.modal-backdrop' ).not( '.fv-modal-stack' ).css( 'z-index', 1039 + (10 * $('body').data( 'fv_open_modals' )));
$( '.modal-backdrop' ).not( 'fv-modal-stack' ).addClass( 'fv-modal-stack' );
$(this).addClass('fv-modal-stack');
// Increment the number of open modals
$('body').data('fv_open_modals', $('body').data('fv_open_modals') + 1);
// Setup the appropriate z-index
$(this).css('z-index', 1040 + (10 * $('body').data('fv_open_modals')));
$('.modal-backdrop').not('.fv-modal-stack').css('z-index', 1039 + (10 * $('body').data('fv_open_modals')));
$('.modal-backdrop').not('fv-modal-stack').addClass('fv-modal-stack');
});
$.fn.modal.Constructor.prototype.enforceFocus = function() {
$( document )
.off( 'focusin.bs.modal' ) // guard against infinite focus loop
.on( 'focusin.bs.modal', $.proxy( function( e ) {
if (
this.$element[ 0 ] !== e.target && !this.$element.has( e.target ).length
// CKEditor compatibility fix start.
&& !$( e.target ).closest( '.cke_dialog, .cke' ).length
// CKEditor compatibility fix end.
) {
this.$element.trigger( 'focus' );
}
}, this ) );
};
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function(e) {
if (
this.$element[0] !== e.target && !this.$element.has(e.target).length
// CKEditor compatibility fix start.
&& !$(e.target).closest('.cke_dialog, .cke').length
// CKEditor compatibility fix end.
) {
this.$element.trigger('focus');
}
}, this));
};
load()
})

View file

@ -1,24 +1,24 @@
$(document).ready(function(){
$("#apiResetForm").submit(function(e){
$(document).ready(function() {
$("#apiResetForm").submit(function(e) {
$.post("/api/reset", $(this).serialize())
.done(function(data){
api_key = data.data
successFlash(data.message)
$("#api_key").val(api_key)
})
.fail(function(data){
errorFlash(data.message)
})
.done(function(data) {
api_key = data.data
successFlash(data.message)
$("#api_key").val(api_key)
})
.fail(function(data) {
errorFlash(data.message)
})
return false
})
$("#settingsForm").submit(function(e){
$("#settingsForm").submit(function(e) {
$.post("/settings", $(this).serialize())
.done(function(data){
successFlash(data.message)
})
.fail(function(data){
errorFlash(data.responseJSON.message)
})
.done(function(data) {
successFlash(data.message)
})
.fail(function(data) {
errorFlash(data.responseJSON.message)
})
return false
})
})

View file

@ -1,64 +1,66 @@
var templates = []
var icons = {
"application/vnd.ms-excel" : "fa-file-excel-o",
"text/plain" : "fa-file-text-o",
"image/gif" : "fa-file-image-o",
"image/png" : "fa-file-image-o",
"application/pdf" : "fa-file-pdf-o",
"application/x-zip-compressed" : "fa-file-archive-o",
"application/x-gzip" : "fa-file-archive-o",
"application/vnd.openxmlformats-officedocument.presentationml.presentation" : "fa-file-powerpoint-o",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "fa-file-word-o",
"application/octet-stream" : "fa-file-o",
"application/x-msdownload" : "fa-file-o"
"application/vnd.ms-excel": "fa-file-excel-o",
"text/plain": "fa-file-text-o",
"image/gif": "fa-file-image-o",
"image/png": "fa-file-image-o",
"application/pdf": "fa-file-pdf-o",
"application/x-zip-compressed": "fa-file-archive-o",
"application/x-gzip": "fa-file-archive-o",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "fa-file-powerpoint-o",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "fa-file-word-o",
"application/octet-stream": "fa-file-o",
"application/x-msdownload": "fa-file-o"
}
// Save attempts to POST to /templates/
function save(idx){
var template = {attachments:[]}
function save(idx) {
var template = {
attachments: []
}
template.name = $("#name").val()
template.subject = $("#subject").val()
template.html = CKEDITOR.instances["html_editor"].getData();
// Fix the URL Scheme added by CKEditor (until we can remove it from the plugin)
template.html = template.html.replace(/https?:\/\/{{\.URL}}/gi, "{{.URL}}")
// If the "Add Tracker Image" checkbox is checked, add the tracker
if ($("#use_tracker_checkbox").prop("checked") &&
template.html.indexOf("{{.Tracker}}") == -1 &&
template.html.indexOf("{{.TrackingUrl}}") == -1){
template.html = template.html.replace("</body>", "{{.Tracker}}</body>")
// If the "Add Tracker Image" checkbox is checked, add the tracker
if ($("#use_tracker_checkbox").prop("checked") &&
template.html.indexOf("{{.Tracker}}") == -1 &&
template.html.indexOf("{{.TrackingUrl}}") == -1) {
template.html = template.html.replace("</body>", "{{.Tracker}}</body>")
}
template.text = $("#text_editor").val()
// Add the attachments
$.each($("#attachmentsTable").DataTable().rows().data(), function(i, target){
// Add the attachments
$.each($("#attachmentsTable").DataTable().rows().data(), function(i, target) {
template.attachments.push({
name : target[1],
name: target[1],
content: target[3],
type: target[4],
})
})
if (idx != -1){
if (idx != -1) {
template.id = templates[idx].id
api.templateId.put(template)
.success(function(data){
successFlash("Template edited successfully!")
load()
dismiss()
})
.success(function(data) {
successFlash("Template edited successfully!")
load()
dismiss()
})
} else {
// Submit the template
api.templates.post(template)
.success(function(data){
successFlash("Template added successfully!")
load()
dismiss()
})
.error(function(data){
modalError(data.responseJSON.message)
})
.success(function(data) {
successFlash("Template added successfully!")
load()
dismiss()
})
.error(function(data) {
modalError(data.responseJSON.message)
})
}
}
function dismiss(){
function dismiss() {
$("#modal\\.flashes").empty()
$("#attachmentsTable").dataTable().DataTable().clear().draw()
$("#name").val("")
@ -67,24 +69,36 @@ function dismiss(){
$("#modal").modal('hide')
}
function deleteTemplate(idx){
if (confirm("Delete " + templates[idx].name + "?")){
function deleteTemplate(idx) {
if (confirm("Delete " + templates[idx].name + "?")) {
api.templateId.delete(templates[idx].id)
.success(function(data){
successFlash(data.message)
load()
})
.success(function(data) {
successFlash(data.message)
load()
})
}
}
function attach(files){
attachmentsTable = $("#attachmentsTable").DataTable();
$.each(files, function(i, file){
function attach(files) {
attachmentsTable = $("#attachmentsTable").DataTable({
destroy: true,
"order": [
[1, "asc"]
],
columnDefs: [{
orderable: false,
targets: "no-sort"
}, {
sClass: "datatable_hidden",
targets: [3, 4]
}]
});
$.each(files, function(i, file) {
var reader = new FileReader();
/* Make this a datatable */
reader.onload = function(e){
reader.onload = function(e) {
var icon = icons[file.type] || "fa-file-o"
// Add the record to the modal
// Add the record to the modal
attachmentsTable.row.add([
'<i class="fa ' + icon + '"></i>',
file.name,
@ -100,33 +114,40 @@ function attach(files){
})
}
function edit(idx){
$("#modalSubmit").unbind('click').click(function(){save(idx)})
$("#attachmentUpload").unbind('click').click(function(){this.value=null})
function edit(idx) {
$("#modalSubmit").unbind('click').click(function() {
save(idx)
})
$("#attachmentUpload").unbind('click').click(function() {
this.value = null
})
$("#html_editor").ckeditor()
$("#attachmentsTable").show()
attachmentsTable = null
if ( $.fn.dataTable.isDataTable('#attachmentsTable') ) {
attachmentsTable = $('#attachmentsTable').DataTable();
attachmentsTable = $('#attachmentsTable').DataTable({
destroy: true,
"order": [
[1, "asc"]
],
columnDefs: [{
orderable: false,
targets: "no-sort"
}, {
sClass: "datatable_hidden",
targets: [3, 4]
}]
});
var template = {
attachments: []
}
else {
attachmentsTable = $("#attachmentsTable").DataTable({
"aoColumnDefs" : [{
"targets" : [3,4],
"sClass" : "datatable_hidden"
}]
});
}
var template = {attachments:[]}
if (idx != -1) {
template = templates[idx]
$("#name").val(template.name)
$("#subject").val(template.subject)
$("#subject").val(template.subject)
$("#html_editor").val(template.html)
$("#text_editor").val(template.text)
$.each(template.attachments, function(i, file){
$.each(template.attachments, function(i, file) {
var icon = icons[file.type] || "fa-file-o"
// Add the record to the modal
// Add the record to the modal
attachmentsTable.row.add([
'<i class="fa ' + icon + '"></i>',
file.name,
@ -137,110 +158,114 @@ function edit(idx){
})
}
// Handle Deletion
$("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function(){
attachmentsTable.row( $(this).parents('tr') )
.remove()
.draw();
$("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function() {
attachmentsTable.row($(this).parents('tr'))
.remove()
.draw();
})
}
function importEmail(){
function importEmail() {
raw = $("#email_content").val()
if (!raw){
if (!raw) {
modalError("No Content Specified!")
} else {
$.ajax({
type: "POST",
url: "/api/import/email",
data: raw,
dataType: "json",
contentType: "text/plain"
})
.success(function(data){
$("#text_editor").val(data.text)
$("#html_editor").val(data.html)
$("#subject").val(data.subject)
$("#importEmailModal").modal("hide")
})
.error(function(data){
modalError(data.responseJSON.message)
})
$.ajax({
type: "POST",
url: "/api/import/email",
data: raw,
dataType: "json",
contentType: "text/plain"
})
.success(function(data) {
$("#text_editor").val(data.text)
$("#html_editor").val(data.html)
$("#subject").val(data.subject)
$("#importEmailModal").modal("hide")
})
.error(function(data) {
modalError(data.responseJSON.message)
})
}
}
function load(){
function load() {
$("#templateTable").hide()
$("#emptyMessage").hide()
$("#loading").show()
api.templates.get()
.success(function(ts){
templates = ts
$("#loading").hide()
if (templates.length > 0){
$("#templateTable").show()
templateTable = $("#templateTable").DataTable();
templateTable.clear()
$.each(templates, function(i, template){
templateTable.row.add([
template.name,
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
.success(function(ts) {
templates = ts
$("#loading").hide()
if (templates.length > 0) {
$("#templateTable").show()
templateTable = $("#templateTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
templateTable.clear()
$.each(templates, function(i, template) {
templateTable.row.add([
template.name,
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
<i class='fa fa-pencil'></i>\
</button>\
<button class='btn btn-danger' onclick='deleteTemplate(" + i + ")'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function(){
$("#loading").hide()
errorFlash("Error fetching templates")
})
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function() {
$("#loading").hide()
errorFlash("Error fetching templates")
})
}
$(document).ready(function(){
$(document).ready(function() {
// Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function( event ) {
$(this).removeClass( 'fv-modal-stack' );
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) - 1 );
$('.modal').on('hidden.bs.modal', function(event) {
$(this).removeClass('fv-modal-stack');
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
});
$( '.modal' ).on( 'shown.bs.modal', function ( event ) {
$('.modal').on('shown.bs.modal', function(event) {
// Keep track of the number of open modals
if ( typeof( $('body').data( 'fv_open_modals' ) ) == 'undefined' )
{
$('body').data( 'fv_open_modals', 0 );
if (typeof($('body').data('fv_open_modals')) == 'undefined') {
$('body').data('fv_open_modals', 0);
}
// if the z-index of this modal has been set, ignore.
if ( $(this).hasClass( 'fv-modal-stack' ) )
{
if ($(this).hasClass('fv-modal-stack')) {
return;
}
$(this).addClass( 'fv-modal-stack' );
// Increment the number of open modals
$('body').data( 'fv_open_modals', $('body').data( 'fv_open_modals' ) + 1 );
// Setup the appropriate z-index
$(this).css('z-index', 1040 + (10 * $('body').data( 'fv_open_modals' )));
$( '.modal-backdrop' ).not( '.fv-modal-stack' ).css( 'z-index', 1039 + (10 * $('body').data( 'fv_open_modals' )));
$( '.modal-backdrop' ).not( 'fv-modal-stack' ).addClass( 'fv-modal-stack' );
$(this).addClass('fv-modal-stack');
// Increment the number of open modals
$('body').data('fv_open_modals', $('body').data('fv_open_modals') + 1);
// Setup the appropriate z-index
$(this).css('z-index', 1040 + (10 * $('body').data('fv_open_modals')));
$('.modal-backdrop').not('.fv-modal-stack').css('z-index', 1039 + (10 * $('body').data('fv_open_modals')));
$('.modal-backdrop').not('fv-modal-stack').addClass('fv-modal-stack');
});
$.fn.modal.Constructor.prototype.enforceFocus = function() {
$( document )
.off( 'focusin.bs.modal' ) // guard against infinite focus loop
.on( 'focusin.bs.modal', $.proxy( function( e ) {
if (
this.$element[ 0 ] !== e.target && !this.$element.has( e.target ).length
// CKEditor compatibility fix start.
&& !$( e.target ).closest( '.cke_dialog, .cke' ).length
// CKEditor compatibility fix end.
) {
this.$element.trigger( 'focus' );
}
}, this ) );
};
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function(e) {
if (
this.$element[0] !== e.target && !this.$element.has(e.target).length
// CKEditor compatibility fix start.
&& !$(e.target).closest('.cke_dialog, .cke').length
// CKEditor compatibility fix end.
) {
this.$element.trigger('focus');
}
}, this));
};
load()
})

View file

@ -1,59 +1,68 @@
var groups = []
// Save attempts to POST or PUT to /groups/
function save(idx){
function save(idx) {
var targets = []
$.each($("#targetsTable").DataTable().rows().data(), function(i, target){
$.each($("#targetsTable").DataTable().rows().data(), function(i, target) {
targets.push({
first_name : target[0],
first_name: target[0],
last_name: target[1],
email: target[2],
position: target[3]
})
})
var group = {
name: $("#name").val(),
targets: targets
}
// Submit the group
name: $("#name").val(),
targets: targets
}
// Submit the group
if (idx != -1) {
// If we're just editing an existing group,
// we need to PUT /groups/:id
group.id = groups[idx].id
api.groupId.put(group)
.success(function(data){
successFlash("Group updated successfully!")
load()
dismiss()
})
.error(function(data){
modalError(data.responseJSON.message)
})
.success(function(data) {
successFlash("Group updated successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function(data) {
modalError(data.responseJSON.message)
})
} else {
// Else, if this is a new group, POST it
// to /groups
api.groups.post(group)
.success(function(data){
successFlash("Group added successfully!")
load()
dismiss()
})
.error(function(data){
modalError(data.responseJSON.message)
})
.success(function(data) {
successFlash("Group added successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function(data) {
modalError(data.responseJSON.message)
})
}
}
function dismiss(){
function dismiss() {
$("#targetsTable").dataTable().DataTable().clear().draw()
$("#name").val("")
$("#modal\\.flashes").empty()
$("#modal").modal('hide')
}
function edit(idx){
targets = $("#targetsTable").dataTable()
$("#modalSubmit").unbind('click').click(function(){save(idx)})
function edit(idx) {
targets = $("#targetsTable").dataTable({
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
})
$("#modalSubmit").unbind('click').click(function() {
save(idx)
})
if (idx == -1) {
group = {}
} else {
@ -61,31 +70,6 @@ function edit(idx){
$("#name").val(group.name)
$.each(group.targets, function(i, record) {
targets.DataTable()
.row.add([
record.first_name,
record.last_name,
record.email,
record.position,
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
});
}
// Handle file uploads
$("#csvupload").fileupload({
dataType:"json",
add: function(e, data){
$("#modal\\.flashes").empty()
var acceptFileTypes= /(csv|txt)$/i;
var filename = data.originalFiles[0]['name']
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
modalError("Unsupported file extension (use .csv or .txt)")
return false;
}
data.submit();
},
done: function(e, data){
$.each(data.result, function(i, record) {
targets.DataTable()
.row.add([
record.first_name,
record.last_name,
@ -93,87 +77,121 @@ function edit(idx){
record.position,
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
});
}
// Handle file uploads
$("#csvupload").fileupload({
dataType: "json",
add: function(e, data) {
$("#modal\\.flashes").empty()
var acceptFileTypes = /(csv|txt)$/i;
var filename = data.originalFiles[0]['name']
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
modalError("Unsupported file extension (use .csv or .txt)")
return false;
}
data.submit();
},
done: function(e, data) {
$.each(data.result, function(i, record) {
targets.DataTable()
.row.add([
record.first_name,
record.last_name,
record.email,
record.position,
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
});
}
})
}
function deleteGroup(idx){
if (confirm("Delete " + groups[idx].name + "?")){
function deleteGroup(idx) {
if (confirm("Delete " + groups[idx].name + "?")) {
api.groupId.delete(groups[idx].id)
.success(function(data){
successFlash(data.message)
load()
})
.success(function(data) {
successFlash(data.message)
load()
})
}
}
function load(){
function load() {
$("#groupTable").hide()
$("#emptyMessage").hide()
$("#loading").show()
api.groups.get()
.success(function(gs){
$("#loading").hide()
if (gs.length > 0){
groups = gs
$("#emptyMessage").hide()
$("#groupTable").show()
groupTable = $("#groupTable").DataTable();
groupTable.clear();
$.each(groups, function(i, group){
var targets = ""
$.each(group.targets, function(i, target){
targets += target.email + ", "
if (targets.length > 50) {
targets = targets.slice(0,-3) + "..."
return false;
}
})
groupTable.row.add([
group.name,
targets,
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
.success(function(gs) {
$("#loading").hide()
if (gs.length > 0) {
groups = gs
$("#emptyMessage").hide()
$("#groupTable").show()
groupTable = $("#groupTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
groupTable.clear();
$.each(groups, function(i, group) {
var targets = ""
$.each(group.targets, function(i, target) {
targets += target.email + ", "
if (targets.length > 50) {
targets = targets.slice(0, -3) + "..."
return false;
}
})
groupTable.row.add([
group.name,
targets,
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit(" + i + ")'>\
<i class='fa fa-pencil'></i>\
</button>\
<button class='btn btn-danger' onclick='deleteGroup(" + i + ")'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function(){
errorFlash("Error fetching groups")
})
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function() {
errorFlash("Error fetching groups")
})
}
$(document).ready(function(){
$(document).ready(function() {
load()
// Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function(){
// Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function() {
targets.DataTable()
.row.add([
$("#firstName").val(),
$("#lastName").val(),
$("#email").val(),
$("#position").val(),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
])
.draw()
$("#targetForm>div>input").val('')
$("#firstName").focus()
return false
})
// Handle Deletion
$("#targetsTable").on("click", "span>i.fa-trash-o", function() {
targets.DataTable()
.row.add([
$("#firstName").val(),
$("#lastName").val(),
$("#email").val(),
$("#position").val(),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
])
.draw()
$("#targetForm>div>input").val('')
$("#firstName").focus()
return false
.row($(this).parents('tr'))
.remove()
.draw();
})
// Handle Deletion
$("#targetsTable").on("click", "span>i.fa-trash-o", function(){
targets.DataTable()
.row( $(this).parents('tr') )
.remove()
.draw();
$("#modal").on("hide.bs.modal", function() {
dismiss()
})
})

View file

@ -1,18 +1,16 @@
function errorFlash(message) {
$("#flashes").empty()
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>"
)
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
}
function successFlash(message) {
$("#flashes").empty()
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-success\">\
<i class=\"fa fa-check-circle\"></i> " + message + "</div>"
)
<i class=\"fa fa-check-circle\"></i> " + message + "</div>")
}
function modalError(message){
function modalError(message) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
}
@ -23,7 +21,7 @@ function query(endpoint, method, data) {
async: false,
method: method,
data: JSON.stringify(data),
dataType:"json",
dataType: "json",
contentType: "application/json"
})
}
@ -33,117 +31,117 @@ Define our API Endpoints
*/
var api = {
// campaigns contains the endpoints for /campaigns
campaigns : {
campaigns: {
// get() - Queries the API for GET /campaigns
get: function(){
get: function() {
return query("/campaigns/", "GET", {})
},
// post() - Posts a campaign to POST /campaigns
post: function(data){
post: function(data) {
return query("/campaigns/", "POST", data)
}
},
// campaignId contains the endpoints for /campaigns/:id
campaignId : {
campaignId: {
// get() - Queries the API for GET /campaigns/:id
get: function(id){
get: function(id) {
return query("/campaigns/" + id, "GET", {})
},
// delete() - Deletes a campaign at DELETE /campaigns/:id
delete: function(id){
return query("/campaigns/" + id, "DELETE", data)
delete: function(id) {
return query("/campaigns/" + id, "DELETE", {})
}
},
// groups contains the endpoints for /groups
groups : {
groups: {
// get() - Queries the API for GET /groups
get: function(){
get: function() {
return query("/groups/", "GET", {})
},
// post() - Posts a campaign to POST /groups
post: function(group){
post: function(group) {
return query("/groups/", "POST", group)
}
},
// groupId contains the endpoints for /groups/:id
groupId : {
groupId: {
// get() - Queries the API for GET /groups/:id
get: function(id){
get: function(id) {
return query("/groups/" + id, "GET", {})
},
// put() - Puts a campaign to PUT /groups/:id
put: function (group){
put: function(group) {
return query("/groups/" + group.id, "PUT", group)
},
// delete() - Deletes a campaign at DELETE /groups/:id
delete: function(id){
delete: function(id) {
return query("/groups/" + id, "DELETE", {})
}
},
// templates contains the endpoints for /templates
templates : {
templates: {
// get() - Queries the API for GET /templates
get: function(){
get: function() {
return query("/templates/", "GET", {})
},
// post() - Posts a campaign to POST /templates
post: function(template){
post: function(template) {
return query("/templates/", "POST", template)
}
},
// templateId contains the endpoints for /templates/:id
templateId : {
templateId: {
// get() - Queries the API for GET /templates/:id
get: function(id){
get: function(id) {
return query("/templates/" + id, "GET", {})
},
// put() - Puts a campaign to PUT /templates/:id
put: function (template){
put: function(template) {
return query("/templates/" + template.id, "PUT", template)
},
// delete() - Deletes a campaign at DELETE /templates/:id
delete: function(id){
delete: function(id) {
return query("/templates/" + id, "DELETE", {})
}
},
// pages contains the endpoints for /pages
pages : {
pages: {
// get() - Queries the API for GET /pages
get: function(){
get: function() {
return query("/pages/", "GET", {})
},
// post() - Posts a campaign to POST /pages
post: function(page){
post: function(page) {
return query("/pages/", "POST", page)
}
},
// templateId contains the endpoints for /templates/:id
pageId : {
pageId: {
// get() - Queries the API for GET /templates/:id
get: function(id){
get: function(id) {
return query("/pages/" + id, "GET", {})
},
// put() - Puts a campaign to PUT /templates/:id
put: function (page){
put: function(page) {
return query("/pages/" + page.id, "PUT", page)
},
// delete() - Deletes a campaign at DELETE /templates/:id
delete: function(id){
delete: function(id) {
return query("/pages/" + id, "DELETE", {})
}
},
// import handles all of the "import" functions in the api
import_email : function(raw) {
return query("/import/email", "POST", {})
import_email: function(raw) {
return query("/import/email", "POST", {})
},
clone_site : function(req){
return query("/import/site", "POST", req)
clone_site: function(req) {
return query("/import/site", "POST", req)
}
}
// Register our moment.js datatables listeners
$(document).ready(function(){
$.fn.dataTable.moment('MMMM Do YYYY, h:mm:ss a');
// Setup tooltips
$('[data-toggle="tooltip"]').tooltip()
$(document).ready(function() {
$.fn.dataTable.moment('MMMM Do YYYY, h:mm:ss a');
// Setup tooltips
$('[data-toggle="tooltip"]').tooltip()
});

6
static/js/papaparse.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -30,22 +30,11 @@
<h1 class="page-header" id="page-title">Results for campaign.name</h1>
</div>
<div class="row">
<!--
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-cogs fa-lg"></i>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="#">Export</a>
</li>
<li><a href="#">Relaunch</a>
</li>
</ul>
</div>
-->
<button type="button" class="btn btn-danger" data-toggle="tooltip" data-placement="right" title="Delete Campaign" onclick="deleteCampaign()">
<i class="fa fa-times fa-lg"></i>
<button type="button" id="exportButton" class="btn btn-primary" onclick="exportAsCSV()">
<i class="fa fa-file-excel-o"></i> Export CSV
</button>
<button type="button" class="btn btn-danger" data-toggle="tooltip" onclick="deleteCampaign()">
<i class="fa fa-trash-o fa-lg"></i> Delete
</button>
</div>
<br />
@ -114,5 +103,6 @@
<script src="/js/d3.min.js"></script>
<script src="/js/topojson.min.js"></script>
<script src="/js/datamaps.min.js"></script>
<script src="/js/papaparse.min.js"></script>
<script src="/js/app/campaign_results.js"></script>
{{end}}

View file

@ -47,7 +47,7 @@
<th>Name</th>
<th>Created Date</th>
<th>Status</th>
<th class="col-md-2"></th>
<th class="col-md-2 no-sort"></th>
</tr>
</thead>
<tbody>
@ -113,7 +113,7 @@
<table id="groupTable" class="table table-hover table-striped table-condensed">
<thead>
<th>Group Name</th>
<th></th>
<th class="no-sort"></th>
<tbody>
</tbody>
</table>
@ -121,7 +121,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="save()">Save changes</button>
<button type="button" class="btn btn-primary" onclick="launch()"><i class="fa fa-envelope"></i> Launch Campaign</button>
</div>
</div>
</div>

View file

@ -67,7 +67,7 @@
<th>Name</th>
<th>Created Date</th>
<th>Status</th>
<th class="col-md-2 col-sm-2"></th>
<th class="col-md-2 col-sm-2 no-sort"></th>
</tr>
</thead>
<tbody>

View file

@ -44,7 +44,7 @@
<tr>
<th>Name</th>
<th>Last Modified Date</th>
<th class="col-md-2"></th>
<th class="col-md-2 no-sort"></th>
</tr>
</thead>
<tbody>

View file

@ -46,7 +46,7 @@
<tr>
<th>Name</th>
<th>Modified Date</th>
<th class="col-md-2"></th>
<th class="col-md-2 no-sort"></th>
</tr>
</thead>
<tbody>
@ -102,11 +102,11 @@
<table id="attachmentsTable" class="table">
<thead>
<tr>
<th class="col-md-1"></th>
<th class="col-md-1 no-sort"></th>
<th class="col-md-10">Name</th>
<th class="col-md-1"></th>
<th class="datatable_hidden">Content</th>
<th class="datatable_hidden">Type</th>
<th class="col-md-1 no-sort"></th>
<th class="datatable_hidden no-sort">Content</th>
<th class="datatable_hidden no-sort">Type</th>
</tr>
</thead>
<tbody>

View file

@ -47,7 +47,7 @@
<th>Name</th>
<th>Members</th>
<th>Modified Date</th>
<th class="col-md-2"></th>
<th class="col-md-2 no-sort"></th>
</tr>
</thead>
<tbody>
@ -94,20 +94,20 @@
</form>
</div>
<br />
<table id="targetsTable" class="table table-hover table-striped">
<table id="targetsTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Position</th>
<th></th>
<th class="no-sort"></th>
<tbody>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" onclick="dismiss()">Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
</div>
</div>

28
util/doc.go Normal file
View file

@ -0,0 +1,28 @@
/*
gophish - Open-Source Phishing Framework
The MIT License (MIT)
Copyright (c) 2013 Jordan Wright
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Package util provides misc. utility functions for gophish
package util

28
worker/doc.go Normal file
View file

@ -0,0 +1,28 @@
/*
gophish - Open-Source Phishing Framework
The MIT License (MIT)
Copyright (c) 2013 Jordan Wright
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Package worker contains the functionality for the background worker process.
package worker

View file

@ -9,8 +9,8 @@ import (
"strings"
"text/template"
"github.com/jordan-wright/email"
"github.com/gophish/gophish/models"
"github.com/jordan-wright/email"
)
// Logger is the logger for the worker
@ -119,6 +119,10 @@ func processCampaign(c *models.Campaign) {
if err != nil {
Logger.Println(err)
}
err = c.AddEvent(models.Event{Email: t.Email, Message: models.EVENT_SENT})
if err != nil {
Logger.Println(err)
}
}
}
err = c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)