Heavy commit of qrz
All checks were successful
continuous-integration/drone/push Build is passing

- SQL injection protection with regex and prepared statements
- Handled new website
- Refactored ws
- CSV export handle
- modified HTML, CSS and JS
This commit is contained in:
Paul 2020-05-23 12:32:36 +02:00
parent be699390b6
commit 4239c1dbb1
11 changed files with 290 additions and 145 deletions

View File

@ -23,12 +23,13 @@ db_name="database"
db_username="username" db_username="username"
db_password="password" db_password="password"
db_table="qrz_test" db_table="qrz_test"
cron="@every 1h"
``` ```
### Run ### Run
```bash ```bash
./qrz -configfile qrz.ini ./qrz -configfile qrz.ini -port 8080
``` ```
## License ## License

View File

@ -11,9 +11,12 @@ import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
var version string
func main() { func main() {
var config config.Config var config config.Config
config.GetConfig() config.GetConfig()
config.Version = version
err := database.Initialize(&config) err := database.Initialize(&config)
defer config.Db.Close() defer config.Db.Close()

View File

@ -4,5 +4,4 @@ db_name="database"
db_username="username" db_username="username"
db_password="password" db_password="password"
db_table="qrz_test" db_table="qrz_test"
port=8080
cron="@every 1h" cron="@every 1h"

View File

@ -12,10 +12,14 @@ import (
// GetConfig fetch configuration // GetConfig fetch configuration
func (config *Config) GetConfig() error { func (config *Config) GetConfig() error {
var configfile string var configfile string
var nofeed bool
var port int
flag.Usage = utils.Usage flag.Usage = utils.Usage
flag.StringVar(&configfile, "configfile", "qrz.ini", "config file to use with qrz section") flag.StringVar(&configfile, "configfile", "qrz.ini", "config file to use with qrz section")
flag.IntVar(&port, "port", 8080, "web port to use")
flag.BoolVar(&nofeed, "nofeed", false, "no feed database table with entries at first launch")
flag.Parse() flag.Parse()
cfg, err := ini.Load(configfile) cfg, err := ini.Load(configfile)
@ -25,7 +29,8 @@ func (config *Config) GetConfig() error {
qrzsection := cfg.Section("qrz") qrzsection := cfg.Section("qrz")
config.Port = qrzsection.Key("port").MustInt(8080) config.Port = port
config.NoFeed = nofeed
config.DbHostname = qrzsection.Key("db_hostname").MustString("localhost") config.DbHostname = qrzsection.Key("db_hostname").MustString("localhost")
config.DbName = qrzsection.Key("db_name").MustString("database") config.DbName = qrzsection.Key("db_name").MustString("database")
config.DbUsername = qrzsection.Key("db_username").MustString("username") config.DbUsername = qrzsection.Key("db_username").MustString("username")
@ -34,37 +39,69 @@ func (config *Config) GetConfig() error {
config.DbTable = qrzsection.Key("db_table").MustString("qrz") config.DbTable = qrzsection.Key("db_table").MustString("qrz")
config.Cron = qrzsection.Key("cron").MustString("@every 1h") config.Cron = qrzsection.Key("cron").MustString("@every 1h")
config.DbSchema = fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (`id` int(8) NOT NULL AUTO_INCREMENT, `qrz` varchar(25) NOT NULL, `name` varchar(25) DEFAULT NULL, `address` varchar(50) DEFAULT NULL, `city` varchar(50) DEFAULT NULL, `zipcode` varchar(5) DEFAULT NULL, `dept` varchar(50) DEFAULT NULL, `country` varchar(25) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `qrz` (`qrz`,`name`,`city`,`dept`) USING BTREE, KEY `test` (`country`), FULLTEXT KEY `city` (`city`), FULLTEXT KEY `dept` (`dept`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;", config.DbTable) config.DbSchema = fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s
(
id int(8) NOT NULL AUTO_INCREMENT,
qrz varchar(25) NOT NULL,
name varchar(25) DEFAULT NULL,
address varchar(50) DEFAULT NULL,
city varchar(50) DEFAULT NULL,
zipcode varchar(5) DEFAULT NULL,
dept varchar(50) DEFAULT NULL,
country varchar(25) DEFAULT NULL,
dmrid varchar(25) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY qrz (qrz,name,city,dept) USING BTREE,
KEY test (country),
FULLTEXT KEY city (city),
FULLTEXT KEY dept (dept)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
config.DbTable)
config.Statements.DbInsert = "INSERT IGNORE INTO %s (qrz,name,city,dept,country) VALUES ('%s','%s','%s','%s','%s');" config.DbStatements.Insert = fmt.Sprintf(
`INSERT IGNORE INTO %s (qrz, dmrid, name, city, dept, country)
VALUES (?,?,?,?,?,?);`,
config.DbTable)
config.DbStatements.ExportCSV = fmt.Sprintf(
`SELECT qrz, name, city, dept, country
FROM %s;`,
config.DbTable)
config.Statements.DbGetQrz = "SELECT qrz FROM %s;" config.DbStatements.Countries = fmt.Sprintf(
`SELECT country
FROM %s
GROUP BY country;`,
config.DbTable)
config.Statements.DbCheck = "SELECT COUNT(*) FROM %s WHERE qrz = '%s';" config.URLBase = `http://groupe-frs.hamstation.eu/index_qrz_liste_%s.php`
config.URLBase = "http://groupe-frs.hamstation.eu/index_qrz_liste_%s.htm"
config.QrzGroups = []string{"01", "03", "09", "103", "104", "107", "119", "13", "14", "146", "147", "15", "156", "16", "161", "163", "18", "188", "2", "214", "233", "25", "26", "29", "30", "31", "32", "34", "43", "44", "49", "54", "64", "66", "76", "79", "84", "97", "98"} config.QrzGroups = []string{"01", "03", "09", "103", "104", "107", "119", "13", "14", "146", "147", "15", "156", "16", "161", "163", "18", "188", "2", "214", "233", "25", "26", "29", "30", "31", "32", "34", "43", "44", "49", "54", "64", "66", "76", "79", "84", "97", "98"}
//config.QrzGroups = map[string]string{"France": "14"}
return nil return nil
} }
// Config is the global config of g2g // Config is the global config of qrz
type Config struct { type Config struct {
Port int Db *sqlx.DB
DbHostname string DbHostname string
DbName string DbName string
DbUsername string DbUsername string
DbPassword string DbPassword string
DbSchema string DbSchema string
DbTable string DbTable string
Statements struct { DbStatements struct {
DbInsert string Insert string
DbGetQrz string ExportCSV string
DbCheck string Countries string
} }
URLBase string URLBase string
QrzGroups []string QrzGroups []string
Cron string Cron string
Db *sqlx.DB Port int
NoFeed bool
Version string
} }

View File

@ -1,6 +1,7 @@
package qrz package qrz
import ( import (
"database/sql"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -11,7 +12,6 @@ import (
"git.paulbsd.com/paulbsd/qrz/src/config" "git.paulbsd.com/paulbsd/qrz/src/config"
"github.com/antchfx/htmlquery" "github.com/antchfx/htmlquery"
"github.com/robfig/cron" "github.com/robfig/cron"
"golang.org/x/text/encoding/charmap"
) )
// InitCronConfig create task schedules // InitCronConfig create task schedules
@ -74,12 +74,13 @@ func getBody(url string) (string, error) {
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
/*
// For old website support 23/05/2020
dec := charmap.ISO8859_1.NewDecoder() dec := charmap.ISO8859_1.NewDecoder()
output, _ := dec.Bytes(body) output, _ := dec.Bytes(body)
bodyString := string(output) bodyString := string(output)
*/
return bodyString, nil return string(body), nil
} }
// getFrsEntries get FRS entries from html body // getFrsEntries get FRS entries from html body
@ -94,14 +95,25 @@ func getFrsEntries(config config.Config, body string) (frsentries map[string]Frs
for _, n := range htmlquery.Find(htmlpage, "//tr") { for _, n := range htmlquery.Find(htmlpage, "//tr") {
td := htmlquery.Find(n, "//td") td := htmlquery.Find(n, "//td")
if re.MatchString(htmlquery.InnerText(td[0])) { if len(td) > 2 {
if re.MatchString(htmlquery.InnerText(td[1])) {
frs := FrsEntry{ frs := FrsEntry{
QRZ: strings.Replace(htmlquery.InnerText(td[0]), "'", "\\'", -1), QRZ: strings.TrimLeft(htmlquery.InnerText(td[1]), " "),
Name: strings.Replace(htmlquery.InnerText(td[1]), "'", "\\'", -1), DMRID: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[0]), " "), Valid: true},
City: strings.Replace(htmlquery.InnerText(td[2]), "'", "\\'", -1), Name: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
Dept: strings.Replace(htmlquery.InnerText(td[3]), "'", "\\'", -1), City: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
Country: strings.Replace(htmlquery.InnerText(td[4]), "'", "\\'", -1)} Dept: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true},
Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[5]), " "), Valid: true}}
frsentries[frs.QRZ] = frs frsentries[frs.QRZ] = frs
} else if re.MatchString(htmlquery.InnerText(td[0])) {
frs := FrsEntry{
QRZ: strings.TrimLeft(htmlquery.InnerText(td[0]), " "),
Name: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[1]), " "), Valid: true},
City: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
Dept: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true}}
frsentries[frs.QRZ] = frs
}
} }
} }
@ -109,24 +121,16 @@ func getFrsEntries(config config.Config, body string) (frsentries map[string]Frs
} }
// getCurrentEntries fetch existing entries from database // getCurrentEntries fetch existing entries from database
func getCurrentEntries(config config.Config) (existingQRZ []string, err error) { func getCurrentEntries(config config.Config) (existingQRZ []FrsEntry, err error) {
q := fmt.Sprintf(config.Statements.DbGetQrz, config.DbTable) err = config.Db.Select(&existingQRZ, fmt.Sprintf("SELECT * FROM %s;", config.DbTable))
rows, err := config.Db.Query(q)
defer rows.Close()
for rows.Next() {
var i string
rows.Scan(&i)
existingQRZ = append(existingQRZ, i)
}
return return
} }
// discardExistingEntries remove existing entries from original map[string]FrsEntry // discardExistingEntries remove existing entries from original map[string]FrsEntry
func discardExistingEntries(config config.Config, frsPeople *map[string]FrsEntry, existingQRZ []string) (err error) { func discardExistingEntries(config config.Config, frsPeople *map[string]FrsEntry, existingQRZ []FrsEntry) (err error) {
for _, entry := range existingQRZ { for _, entry := range existingQRZ {
delete(*frsPeople, entry) delete(*frsPeople, entry.QRZ)
} }
return return
@ -139,9 +143,8 @@ func insertFrsEntryToDB(config config.Config, frsPeople map[string]FrsEntry) (er
fmt.Println(fmt.Sprintf("Starting inserts of %d entries", len(frsPeople))) fmt.Println(fmt.Sprintf("Starting inserts of %d entries", len(frsPeople)))
for _, j := range frsPeople { for _, frs := range frsPeople {
query := fmt.Sprintf(config.Statements.DbInsert, config.DbTable, j.QRZ, j.Name, j.City, j.Dept, j.Country) tx.MustExec(config.DbStatements.Insert, frs.QRZ, frs.DMRID, frs.Name, frs.City, frs.Dept, frs.Country)
tx.MustExec(query)
qrzNum++ qrzNum++
} }
@ -156,9 +159,13 @@ func insertFrsEntryToDB(config config.Config, frsPeople map[string]FrsEntry) (er
// FrsEntry describe FRS people // FrsEntry describe FRS people
type FrsEntry struct { type FrsEntry struct {
ID int `db:"id"`
QRZ string `db:"qrz"` QRZ string `db:"qrz"`
Name string `db:"name"` DMRID sql.NullString `db:"dmrid"`
City string `db:"city"` Name sql.NullString `db:"name"`
Dept string `db:"dept"` Address sql.NullString `db:"address"`
Country string `db:"country"` City sql.NullString `db:"city"`
ZipCode sql.NullString `db:"zipcode"`
Dept sql.NullString `db:"dept"`
Country sql.NullString `db:"country"`
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"regexp"
"strings" "strings"
"git.paulbsd.com/paulbsd/qrz/src/config" "git.paulbsd.com/paulbsd/qrz/src/config"
@ -12,6 +13,7 @@ import (
"git.paulbsd.com/paulbsd/qrz/src/static" "git.paulbsd.com/paulbsd/qrz/src/static"
"git.paulbsd.com/paulbsd/qrz/src/templates" "git.paulbsd.com/paulbsd/qrz/src/templates"
"github.com/gobuffalo/packr/v2" "github.com/gobuffalo/packr/v2"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -35,12 +37,21 @@ func RunServer(config config.Config) (err error) {
e.POST("/qrzws", func(c echo.Context) (err error) { e.POST("/qrzws", func(c echo.Context) (err error) {
res, err := Run(c, config) res, err := Run(c, config)
if err != nil { if err != nil {
log.Fatalln(err) return c.String(http.StatusInternalServerError, "Erreur 500 ta mère")
} }
return c.JSON(http.StatusOK, res) return c.JSON(http.StatusOK, res)
}) })
e.GET("/export_frs.csv", func(c echo.Context) (err error) {
data, mime, err := RunCSVExport(c, config)
if err != nil {
log.Fatalln(err)
}
return c.Blob(http.StatusOK, mime, data)
})
if !config.NoFeed {
go qrz.Run(config) go qrz.Run(config)
}
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Port))) e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Port)))
return return
} }
@ -49,15 +60,16 @@ func RunServer(config config.Config) (err error) {
func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err error) { func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err error) {
qrzinputjson := new(QrzDatatableInput) qrzinputjson := new(QrzDatatableInput)
var count int var count int
var countfiltered int
var counttotal int
if err = c.Bind(qrzinputjson); err != nil { if err = c.Bind(qrzinputjson); err != nil {
return return
} }
query, querycountfiltered, querycounttotal, err := BuildQuery(config, *qrzinputjson)
rows, err := config.Db.Queryx(query) rows, err := BuildQuery(config, *qrzinputjson)
if err != nil {
fmt.Println(err)
return
}
for rows.Next() { for rows.Next() {
var line []string var line []string
results := make(map[string]interface{}) results := make(map[string]interface{})
@ -71,55 +83,87 @@ func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err erro
} }
rows.Close() rows.Close()
rowscountfiltered, err := config.Db.Queryx(querycountfiltered) res.RecordsFiltered, err = BuildQueryCountFiltered(config, *qrzinputjson)
rowscountfiltered.Next() res.RecordsTotal, err = BuildQueryCountTotal(config, *qrzinputjson)
err = rowscountfiltered.Scan(&countfiltered)
rowscountfiltered.Close()
rowscounttotal, err := config.Db.Queryx(querycounttotal)
rowscounttotal.Next()
err = rowscounttotal.Scan(&counttotal)
rowscounttotal.Close()
res.RecordsFiltered = countfiltered
res.RecordsTotal = counttotal
res.Draw = qrzinputjson.Draw res.Draw = qrzinputjson.Draw
return return
} }
// BuildQuery builds query for SQL engine // BuildQuery builds main query
func BuildQuery(config config.Config, qrzdt QrzDatatableInput) (query string, querycountfiltered string, querycounttotal string, err error) { func BuildQuery(config config.Config, qrzdt QrzDatatableInput) (rows *sqlx.Rows, err error) {
selectstatement, err := SetSelectStatement(config, qrzdt) var selectstatement, orderstatement, limitstatement, searchstatement string
orderstatement, err := SetOrderStatement(config, qrzdt) selectstatement, err = SetSelectStatement(config, qrzdt)
limitstatement, err := SetLimitStatement(config, qrzdt) if err != nil {
searchstatement, err := SetSearchStatement(config, qrzdt) return nil, err
}
orderstatement, err = SetOrderStatement(config, qrzdt)
if err != nil {
return nil, err
}
limitstatement, err = SetLimitStatement(config, qrzdt)
if err != nil {
return nil, err
}
searchstatement, err = SetSearchStatement(config, qrzdt)
if err != nil {
return nil, err
}
rows, err = config.Db.Queryx(fmt.Sprintf("SELECT %s FROM %s WHERE %s %s %s;", selectstatement, config.DbTable, searchstatement, orderstatement, limitstatement))
return
}
// BuildQueryCountFiltered builds query for counting filtered
func BuildQueryCountFiltered(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
searchstatement, err := SetSearchStatement(config, qrzdt)
err = config.Db.Get(&cnt, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s;", config.DbTable, searchstatement))
return
}
// BuildQueryCountTotal builds query for counting totals
func BuildQueryCountTotal(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
err = config.Db.Get(&cnt, fmt.Sprintf("SELECT COUNT(*) FROM %s;", config.DbTable))
query = fmt.Sprintf("SELECT %s FROM %s %s %s %s;", selectstatement, config.DbTable, searchstatement, orderstatement, limitstatement)
querycountfiltered = fmt.Sprintf("SELECT COUNT(*) FROM %s %s;", config.DbTable, searchstatement)
querycounttotal = fmt.Sprintf("SELECT COUNT(*) FROM %s;", config.DbTable)
return return
} }
// SetSelectStatement build the sql select statement part // SetSelectStatement build the sql select statement part
func SetSelectStatement(config config.Config, qrzdt QrzDatatableInput) (selectstatement string, err error) { func SetSelectStatement(config config.Config, qrzdt QrzDatatableInput) (selectstatement string, err error) {
var cols []string var cols []string
colre := regexp.MustCompile(`^[a-z]+$`)
if len(qrzdt.Columns) > 0 { if len(qrzdt.Columns) > 0 {
for _, col := range qrzdt.Columns { for _, col := range qrzdt.Columns {
valid := colre.MatchString(col.Name)
if valid {
cols = append(cols, col.Name) cols = append(cols, col.Name)
} else {
err = fmt.Errorf("String does not match prerequisites")
return
}
} }
selectstatement = strings.Join(cols, ",") selectstatement = strings.Join(cols, ",")
} else { } else {
selectstatement = "*" selectstatement = "*"
} }
return return
} }
// SetOrderStatement build the sql order statement part // SetOrderStatement build the sql order statement part
func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt string, err error) { func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt string, err error) {
var orderstmts []string var orderstmts []string
colre := regexp.MustCompile(`^[a-z]+$`)
orderre := regexp.MustCompile(`^(ASC|asc|DESC|desc)$`)
for _, col := range qrzdt.Order { for _, col := range qrzdt.Order {
orderstmts = append(orderstmts, fmt.Sprintf("%s %s", qrzdt.Columns[col.Column].Name, strings.ToUpper(col.Dir))) if colre.MatchString(qrzdt.Columns[col.Column].Name) && orderre.MatchString(col.Dir) {
orderstmts = append(orderstmts, fmt.Sprintf("%s %s", qrzdt.Columns[col.Column].Name, col.Dir))
} else {
err = fmt.Errorf("Order statements does not match prerequisites")
return
}
} }
if len(orderstmts) > 0 { if len(orderstmts) > 0 {
orderstmt = fmt.Sprintf("ORDER BY %s", strings.Join(orderstmts, ", ")) orderstmt = fmt.Sprintf("ORDER BY %s", strings.Join(orderstmts, ", "))
@ -129,10 +173,16 @@ func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt
// SetLimitStatement build the sql limit statement part // SetLimitStatement build the sql limit statement part
func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt string, err error) { func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt string, err error) {
intre := regexp.MustCompile(`^[0-9]+$`)
if qrzdt.Length < 1 { if qrzdt.Length < 1 {
qrzdt.Length = 50 qrzdt.Length = 50
} }
if intre.MatchString(fmt.Sprintf("%d", qrzdt.Length)) && intre.MatchString(fmt.Sprintf("%d", qrzdt.Start)) {
limitstmt = fmt.Sprintf("LIMIT %d OFFSET %d", qrzdt.Length, qrzdt.Start) limitstmt = fmt.Sprintf("LIMIT %d OFFSET %d", qrzdt.Length, qrzdt.Start)
} else {
err = fmt.Errorf("Limit statements does not match prerequisites")
return
}
return return
} }
@ -140,28 +190,37 @@ func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt
func SetSearchStatement(config config.Config, qrzdt QrzDatatableInput) (searchstmt string, err error) { func SetSearchStatement(config config.Config, qrzdt QrzDatatableInput) (searchstmt string, err error) {
var searchstmtslice []string var searchstmtslice []string
if len(qrzdt.Columns) > 0 { if len(qrzdt.Columns) > 0 {
searchstmtslice = append(searchstmtslice, "WHERE")
for id, i := range qrzdt.Columns { for id, i := range qrzdt.Columns {
searchstmtslice = append(searchstmtslice, fmt.Sprintf("%s LIKE '%%%s%%'", i.Name, qrzdt.Search.Value)) searchstmtslice = append(searchstmtslice, fmt.Sprintf("%s LIKE '%%%s%%'", i.Name, qrzdt.Search.Value))
if id < len(qrzdt.Columns)-1 { if id < len(qrzdt.Columns)-1 {
searchstmtslice = append(searchstmtslice, "OR") searchstmtslice = append(searchstmtslice, "OR")
} }
} }
searchstmt = strings.Join(searchstmtslice, " ")
} else {
searchstmtslice = []string{"1=1"}
} }
searchstmt = strings.Join(searchstmtslice, " ")
return return
} }
// getCurrentEntries fetch existing entries from database // RunCSVExport runs the main loop
func getCurrentEntries(config config.Config) (existingQRZ []string, err error) { func RunCSVExport(c echo.Context, config config.Config) (data []byte, mime string, err error) {
q := fmt.Sprintf(config.Statements.DbGetQrz, config.DbTable) mime = "text/csv"
rows, err := config.Db.Query(q) rows, err := config.Db.Queryx(fmt.Sprintf(config.DbStatements.ExportCSV, config.DbTable))
var res []string
for rows.Next() { for rows.Next() {
var qrzelem string var l []string
rows.Scan(&qrzelem) results := make(map[string]interface{})
existingQRZ = append(existingQRZ, qrzelem) err = rows.MapScan(results)
colslice, _ := rows.Columns()
for _, column := range colslice {
l = append(l, fmt.Sprintf("%s", results[column]))
} }
line := strings.Join(l, ",")
res = append(res, line)
}
data = []byte(strings.Join(res, "\n"))
return return
} }

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
static/js/main.js Normal file
View File

@ -0,0 +1,3 @@
function export_frs() {
var w = window.location.href = "/export_frs.csv";
}

View File

@ -17,19 +17,31 @@ $(document).ready(function() {
}, },
"stateSave": true, "stateSave": true,
"ajax": { "ajax": {
"url": "qrzws", "url": "/qrzws",
"type": "POST", "type": "POST",
"contentType": "application/json", "contentType": "application/json",
"data": function (d) { "data": function (d) {
return JSON.stringify(d); return JSON.stringify(d);
}, },
}, },
"columns": [ "columns": [{
{ "name": "qrz" }, "name": "qrz"
{ "name": "name" }, },
{ "name": "city" }, {
{ "name": "dept" }, "name": "name"
{ "name": "country" }, },
{
"name": "city"
},
{
"name": "dept"
},
{
"name": "country"
},
{
"name": "dmrid"
},
] ]
}); });
}); });

View File

@ -1,16 +1,21 @@
<html> <html>
<head> <head>
<link rel="stylesheet" type="text/css" href="static/css/main.css" media="screen"> <link rel="stylesheet" type="text/css" href="static/css/main.css" media="screen">
<link rel="stylesheet" type="text/css" href="static/css/jquery.dataTables.min.css" media="screen"> <link rel="stylesheet" type="text/css" href="static/css/jquery.dataTables.min.css" media="screen">
<link rel="stylesheet" type="text/css" href="static/css/font-awesome/all.css"> <link rel="stylesheet" type="text/css" href="static/css/font-awesome/all.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="static/css/bootstrap.min.css">
<script type="text/javascript" src="static/js/jquery.js"></script> <script type="text/javascript" src="static/js/jquery.js"></script>
<script type="text/javascript" src="static/js/jquery.dataTables.min.js"></script> <script type="text/javascript" src="static/js/jquery.dataTables.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> <script type="text/javascript" src="static/js/bootstrap.min.js"></script>
<script type="text/javascript" src="static/js/main_table.js"></script> <script type="text/javascript" src="static/js/main_table.js"></script>
<script type="text/javascript" src="static/js/main.js"></script>
</head> </head>
<body> <body>
<h1 class="jumbotron-heading">FRS QRZ database</h1> <h1 class="jumbotron-heading">FRS QRZ database</h1>
<p>Mirror of <a href="http://groupe-frs.hamstation.eu">http://groupe-frs.hamstation.eu</a> list of users</p>
<div> <div>
<table id="main_table" class="display cell-border"> <table id="main_table" class="display cell-border">
<thead class="thead-dark"> <thead class="thead-dark">
@ -20,11 +25,16 @@
<td>City</td> <td>City</td>
<td>Department</td> <td>Department</td>
<td>Country</td> <td>Country</td>
<td>DMRID</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
</div> </div>
<div>
<input type="button" value="Export CSV" onclick="export_frs()">
</div>
</body> </body>
</html> </html>