- 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:
parent
be699390b6
commit
4239c1dbb1
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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"
|
@ -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
|
|
||||||
DbName string
|
DbHostname string
|
||||||
DbUsername string
|
DbName string
|
||||||
DbPassword string
|
DbUsername string
|
||||||
DbSchema string
|
DbPassword string
|
||||||
DbTable string
|
DbSchema string
|
||||||
Statements struct {
|
DbTable string
|
||||||
DbInsert string
|
DbStatements struct {
|
||||||
DbGetQrz string
|
Insert string
|
||||||
DbCheck string
|
ExportCSV string
|
||||||
|
Countries string
|
||||||
}
|
}
|
||||||
URLBase string
|
URLBase string
|
||||||
QrzGroups []string
|
QrzGroups []string
|
||||||
Cron string
|
Cron string
|
||||||
Db *sqlx.DB
|
Port int
|
||||||
|
NoFeed bool
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
@ -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,18 +74,19 @@ func getBody(url string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
/*
|
||||||
dec := charmap.ISO8859_1.NewDecoder()
|
// For old website support 23/05/2020
|
||||||
output, _ := dec.Bytes(body)
|
dec := charmap.ISO8859_1.NewDecoder()
|
||||||
bodyString := string(output)
|
output, _ := dec.Bytes(body)
|
||||||
|
bodyString := string(output)
|
||||||
return bodyString, nil
|
*/
|
||||||
|
return string(body), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFrsEntries get FRS entries from html body
|
// getFrsEntries get FRS entries from html body
|
||||||
func getFrsEntries(config config.Config, body string) (frsentries map[string]FrsEntry, err error) {
|
func getFrsEntries(config config.Config, body string) (frsentries map[string]FrsEntry, err error) {
|
||||||
frsentries = make(map[string]FrsEntry)
|
frsentries = make(map[string]FrsEntry)
|
||||||
re := regexp.MustCompile(`^[0-9]{1,4}\s[A-Z]{1,4}\s[0-9]{1,4}`)
|
re := regexp.MustCompile(`^ [0-9]{1,4}\s[A-Z]{1,4}\s[0-9]{1,4}`)
|
||||||
|
|
||||||
htmlpage, err := htmlquery.Parse(strings.NewReader(body))
|
htmlpage, err := htmlquery.Parse(strings.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -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 {
|
||||||
frs := FrsEntry{
|
if re.MatchString(htmlquery.InnerText(td[1])) {
|
||||||
QRZ: strings.Replace(htmlquery.InnerText(td[0]), "'", "\\'", -1),
|
frs := FrsEntry{
|
||||||
Name: strings.Replace(htmlquery.InnerText(td[1]), "'", "\\'", -1),
|
QRZ: strings.TrimLeft(htmlquery.InnerText(td[1]), " "),
|
||||||
City: strings.Replace(htmlquery.InnerText(td[2]), "'", "\\'", -1),
|
DMRID: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[0]), " "), Valid: true},
|
||||||
Dept: strings.Replace(htmlquery.InnerText(td[3]), "'", "\\'", -1),
|
Name: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
|
||||||
Country: strings.Replace(htmlquery.InnerText(td[4]), "'", "\\'", -1)}
|
City: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
|
||||||
frsentries[frs.QRZ] = frs
|
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
|
||||||
|
} 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 {
|
||||||
QRZ string `db:"qrz"`
|
ID int `db:"id"`
|
||||||
Name string `db:"name"`
|
QRZ string `db:"qrz"`
|
||||||
City string `db:"city"`
|
DMRID sql.NullString `db:"dmrid"`
|
||||||
Dept string `db:"dept"`
|
Name sql.NullString `db:"name"`
|
||||||
Country string `db:"country"`
|
Address sql.NullString `db:"address"`
|
||||||
|
City sql.NullString `db:"city"`
|
||||||
|
ZipCode sql.NullString `db:"zipcode"`
|
||||||
|
Dept sql.NullString `db:"dept"`
|
||||||
|
Country sql.NullString `db:"country"`
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
|
||||||
go qrz.Run(config)
|
if !config.NoFeed {
|
||||||
|
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 {
|
||||||
cols = append(cols, col.Name)
|
valid := colre.MatchString(col.Name)
|
||||||
|
if valid {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
limitstmt = fmt.Sprintf("LIMIT %d OFFSET %d", qrzdt.Length, qrzdt.Start)
|
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)
|
||||||
|
} 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
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
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
3
static/js/main.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
function export_frs() {
|
||||||
|
var w = window.location.href = "/export_frs.csv";
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function () {
|
||||||
$('#main_table').DataTable({
|
$('#main_table').DataTable({
|
||||||
"display": true,
|
"display": true,
|
||||||
"cell-border": true,
|
"cell-border": true,
|
||||||
@ -9,27 +9,39 @@ $(document).ready(function() {
|
|||||||
"info": "Showing page _PAGE_ of _PAGES_",
|
"info": "Showing page _PAGE_ of _PAGES_",
|
||||||
"search": "Search :",
|
"search": "Search :",
|
||||||
"paginate": {
|
"paginate": {
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"last": "Last",
|
"last": "Last",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous"
|
"previous": "Previous"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"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"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
@ -1,30 +1,40 @@
|
|||||||
<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>
|
||||||
</head>
|
<script type="text/javascript" src="static/js/main.js"></script>
|
||||||
<body>
|
</head>
|
||||||
<h1 class="jumbotron-heading">FRS QRZ database</h1>
|
|
||||||
<div>
|
<body>
|
||||||
<table id="main_table" class="display cell-border">
|
<h1 class="jumbotron-heading">FRS QRZ database</h1>
|
||||||
<thead class="thead-dark">
|
<p>Mirror of <a href="http://groupe-frs.hamstation.eu">http://groupe-frs.hamstation.eu</a> list of users</p>
|
||||||
<tr>
|
<div>
|
||||||
<td>QRZ</td>
|
<table id="main_table" class="display cell-border">
|
||||||
<td>Name</td>
|
<thead class="thead-dark">
|
||||||
<td>City</td>
|
<tr>
|
||||||
<td>Department</td>
|
<td>QRZ</td>
|
||||||
<td>Country</td>
|
<td>Name</td>
|
||||||
</tr>
|
<td>City</td>
|
||||||
</thead>
|
<td>Department</td>
|
||||||
<tbody>
|
<td>Country</td>
|
||||||
</tbody>
|
<td>DMRID</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
</body>
|
<tbody>
|
||||||
</html>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="button" value="Export CSV" onclick="export_frs()">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user