// checkDeviceID checks the specified device ID and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid). func checkDeviceID(p *page.Params, devIDst string, isCar bool, devices []*ds.Device) (ok bool) { var fieldName string if isCar { fieldName = `<span class="code">Car GPS Device</span>` } else { fieldName = `<span class="code">Personal Mobile GPS Device</span>` } if devIDst == "" { if !isCar { // Personal Mobile device is optional return true } p.ErrorMsg = template.HTML(fieldName + " must be provided! Please select a Device from the list.") return false } var devID int64 var err error if devID, err = strconv.ParseInt(devIDst, 10, 64); err != nil { p.ErrorMsg = template.HTML("Invalid " + fieldName + "! Please select a Device from the list.") return false } // Check if device is owned by the user: for _, d := range devices { if d.KeyID == devID { return true } } p.ErrorMsg = "You do not have access to the specified Device! Please select a Device from the list." return false }
// checkMapPrevSize checks the specified Map preview size string and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid or empty). func checkMapPrevSize(p *page.Params, settingName, mps string) (ok bool) { if mps == "" { return true } basemsg := `Invalid <span class="code">` + settingName + `</span>!` parts := strings.Split(mps, "x") if len(parts) != 2 { p.ErrorMsg = template.HTML(basemsg) return false } for i, part := range parts { name := []string{"width", "height"}[i] num, err := strconv.Atoi(part) if err != nil { p.ErrorMsg = SExecTempl(basemsg+` Invalid number for {{index . 0}}: <span class="highlight">{{index . 1}}</span>`, []interface{}{name, part}) return false } if num < 100 || num > 640 { // 640 is an API limit for free accounts p.ErrorMsg = SExecTempl(basemsg+` {{index . 0}} is outside of valid range (100..640): <span class="highlight">{{index . 1}}</span>`, []interface{}{name, part}) return false } } return true }
// checkName checks the specified Device name and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid). func checkName(p *page.Params, name string) (ok bool) { switch { case len(name) == 0: p.ErrorMsg = template.HTML(`<span class="code">Name</span> must be specified!`) return false case len(name) > 500: p.ErrorMsg = template.HTML(`<span class="code">Name</span> is too long! (cannot be longer than 500 characters)`) return false } return true }
// checkLogsRetention checks the specified logs retention and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid). func checkLogsRetention(p *page.Params, logsRetention string) (ok bool) { basemsg := `Invalid <span class="code">Logs Retention</span>!` num, err := strconv.Atoi(logsRetention) if err != nil { p.ErrorMsg = template.HTML(basemsg) return false } if num < 0 || num > 9999 { p.ErrorMsg = SExecTempl(basemsg+` Value is outside of valid range (0..9,999): <span class="highlight">{{.}}</span>`, num) return false } return true }
// checkContactEmail checks the specified Contact Email and sets an appropriate erorr message // if there's something wrong with it. // Returns true if email is acceptable (valid or empty). func checkContactEmail(p *page.Params, email string) (ok bool) { if email == "" { return true } if len(email) > 500 { p.ErrorMsg = template.HTML(`<span class="code">Contact email</span> is too long! (cannot be longer than 500 characters)`) return false } if _, err := mail.ParseAddressList(email); err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="code">Contact email</span>!`) return false } return true }
// checkAcceptTermsAndPolicy checks the specified accept terms and policty response and sets an appropriate erorr message // if there's something wrong with it. // Returns true if it is acceptable (checked). func checkAcceptTermsAndPolicy(p *page.Params, atap string) (ok bool) { if atap == "" { p.ErrorMsg = "You must accept the Terms and Policy!" return false } return true }
// checkMobPageWidth checks the specified Mobile Page width string and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid or empty). func checkMobPageWidth(p *page.Params, mpw string) (ok bool) { if mpw == "" { return true } basemsg := `Invalid <span class="code">Mobile Page width</span>!` num, err := strconv.Atoi(mpw) if err != nil { p.ErrorMsg = SExecTempl(basemsg+` Invalid number: <span class="highlight">{{.}}</span>`, mpw) return false } if num < 400 || num > 10000 { p.ErrorMsg = SExecTempl(basemsg+` Value is outside of valid range (400..10000): <span class="highlight">{{.}}</span>`, num) return false } return true }
// checkLogsPageSize checks the specified Logs Page Size string and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid or empty). func checkLogsPageSize(p *page.Params, lps string) (ok bool) { if lps == "" { return true } basemsg := `Invalid <span class="code">Logs Table Page Size</span>!` num, err := strconv.Atoi(lps) if err != nil { p.ErrorMsg = SExecTempl(basemsg+` Invalid number: <span class="highlight">{{.}}</span>`, lps) return false } if num < 5 || num > 30 { p.ErrorMsg = SExecTempl(basemsg+` Value is outside of valid range (5..30): <span class="highlight">{{.}}</span>`, num) return false } return true }
// checkSearchPrecision checks the specified search precision and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid). func checkSearchPrecision(p *page.Params, searchPrecision string) (ok bool) { basemsg := `Invalid <span class="code">Search Precision</span>!` num, err := strconv.ParseInt(searchPrecision, 10, 64) if err != nil { p.ErrorMsg = template.HTML(basemsg) return false } if num < 0 || num > 1000*1000 { p.ErrorMsg = SExecTempl(basemsg+` Value is outside of valid range (0..1,000,000): <span class="highlight">{{.}}</span>`, num) return false } if num%100 != 0 { p.ErrorMsg = SExecTempl(basemsg+` Value is not a multiple of 100: <span class="highlight">{{.}}</span>`, num) return false } return true }
// checkGoogleAccounts checks if previous and current Google Accounts match and sets an error message // informing the user that the currently logged in Google Account has changed. // Returns true if Google Accounts match. func checkGoogleAccounts(p *page.Params, googleAccount string) (ok bool) { if googleAccount == p.User.Email { return true } const msg = `The current logged in user <span class="highlight">{{.User}}</span> does not match the <span class="code">Google Account</span>.<br/> This is most likely due to you switched Google Accounts.<br/> Click here to reload the {{.Page.Link}} page.` p.ErrorMsg = SExecTempl(msg, p) return false }
// checkLocationName checks the specified Location name and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid or empty). func checkLocationName(p *page.Params, locnam string) (ok bool) { if locnam == "" { return true } _, err := time.LoadLocation(locnam) if err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="code">Location name</span>!`) return false } return true }
// checkMobMapImgFormat checks the specified Mobile Map image format and sets an appropriate error message // if there's something wrong with it. // Returns true if is acceptable (valid or empty). func checkMobMapImgFormat(p *page.Params, imgFormat string) (ok bool) { if imgFormat == "" { return true } for _, v := range imgFormats { if v == imgFormat { return true } } p.ErrorMsg = template.HTML(`Invalid <span class="code">Mobile Map image format</span>!`) return false }
// logs is the logic implementation of the Logs page. func logs(p *page.Params) { c := p.AppCtx // First get devices var devices []*ds.Device if devices, p.Err = cache.GetDevListForAccKey(c, p.Account.GetKey(c)); p.Err != nil { return } p.Custom["Devices"] = devices fv := p.Request.FormValue p.Custom["Before"] = fv("before") p.Custom["After"] = fv("after") p.Custom["SearchLoc"] = fv("loc") if fv("devID") == "" { // No device chosen yet return } var err error var devID int64 if devID, err = strconv.ParseInt(string(fv("devID")), 10, 64); err != nil { p.ErrorMsg = "Invalid Device! Please select a Device from the list below." return } // Check if device is owned by the user: var dev *ds.Device for _, d := range devices { if d.KeyID == devID { dev = d p.Custom["Device"] = d break } } if dev == nil { p.ErrorMsg = "You do not have access to the specified Device! Please select a Device from the list below." return } // Parse filters: var before time.Time if fv("before") != "" { if before, err = p.ParseTime(timeLayout, strings.TrimSpace(fv("before"))); err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">Before</span>!`) return } // Add 1 second to the parsed time because fraction of a second is not parsed but exists, // so this new time will also include records which has the same time up to the second part and has millisecond part too. before = before.Add(time.Second) } var after time.Time if fv("after") != "" { if after, err = p.ParseTime(timeLayout, strings.TrimSpace(fv("after"))); err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">After</span>!`) return } } var searchLoc appengine.GeoPoint areaCode := int64(-1) if dev.Indexed() && fv("loc") != "" { // GPS coordinates; lat must be in range -90..90, lng must be in range -180..180 baseErr := template.HTML(`Invalid <span class="highlight">Location</span>!`) var coords = strings.Split(strings.TrimSpace(fv("loc")), ",") if len(coords) != 2 { p.ErrorMsg = baseErr return } searchLoc.Lat, err = strconv.ParseFloat(coords[0], 64) if err != nil { p.ErrorMsg = baseErr return } searchLoc.Lng, err = strconv.ParseFloat(coords[1], 64) if err != nil { p.ErrorMsg = baseErr return } if !searchLoc.Valid() { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">Location</span> specified by latitude and longitude! Valid range: [-90, 90] latitude and [-180, 180] longitude`) return } areaCode = AreaCodeForGeoPt(dev.AreaSize, searchLoc.Lat, searchLoc.Lng) } var page int cursorsString := fv("cursors") var cursors = strings.Split(cursorsString, ";")[1:] // Split always returns at least 1 element (and we use semicolon separator before cursors) // Form values if fv("page") == "" { page = 1 } else { page, err = strconv.Atoi(fv("page")) if err != nil || page < 1 { page = 1 } if page > len(cursors) { // If page is provided, so are (should be) the cursors page = len(cursors) } } switch { case fv("submitFirstPage") != "": page = 1 case fv("submitPrevPage") != "": if page > 1 { page-- } case fv("submitNextPage") != "": page++ } pageSize := p.Account.GetLogsPageSize() if ps := fv("pageSize"); ps != "" && ps != strconv.Itoa(pageSize) { // Page size has been changed (on Settings page), drop cursors, render page 1 page = 1 cursorsString = "" cursors = make([]string, 0, 1) } // 'ts all good, proceed with the query: q := datastore.NewQuery(ds.ENameGPS).Filter(ds.PNameDevKeyID+"=", devID) if !before.IsZero() { q = q.Filter(ds.PNameCreated+"<", before) } if !after.IsZero() { q = q.Filter(ds.PNameCreated+">", after) } if areaCode >= 0 { q = q.Filter(ds.PNameAreaCodes+"=", areaCode) } q = q.Order("-" + ds.PNameCreated).Limit(pageSize) var records = make([]*ds.GPS, 0, pageSize) // If there is a cursor, set it. // Page - cursor index mapping: cursors[page-2] // 1st page: no cursor, 2nd page: cursors[0], 3nd page: cursors[1], ... if page > 1 && page <= len(cursors)+1 { var cursor datastore.Cursor if cursor, p.Err = datastore.DecodeCursor(cursors[page-2]); p.Err != nil { return } q = q.Start(cursor) } // Iterate over the results: t := q.Run(c) for { r := new(ds.GPS) _, err := t.Next(r) if err == datastore.Done { break } if err != nil { // Datastore error p.Err = err return } records = append(records, r) r.Dd = -1 // For now, will be set if applicable if r.Track() { // Check the previous (in time) record and calculate distance. // If previous is not a Track, check the one before that etc. for i := len(records) - 2; i >= 0; i-- { if prev := records[i]; prev.Track() { prev.Dd = Distance(r.GeoPoint.Lat, r.GeoPoint.Lng, prev.GeoPoint.Lat, prev.GeoPoint.Lng) prev.Dt = prev.Created.Sub(r.Created) break } } } } if len(records) == 0 { // End of list reached, disable Next page button: p.Custom["EndOfList"] = true } if page == 1 || page > len(cursors) { // Get updated cursor and store it for next page: var cursor datastore.Cursor if cursor, p.Err = t.Cursor(); p.Err != nil { return } cursorString := cursor.String() if page == 1 { // If new records were inserted, they appear on the first page in which case // the cursor for the 2nd page changes (and all other cursors will change). // In this case drop all the cursors: if len(cursors) > 0 && cursors[0] != cursorString { cursorsString = "" cursors = make([]string, 0, 1) } } else { // When end of list is reached, the same cursor will be returned if len(records) == 0 && page == len(cursors)+1 && cursors[page-2] == cursorString { // Add 1 extra, empty page, but not more. if page > 2 && cursors[page-3] == cursorString { // An extra, empty page has already been added, do not add more: page-- } } } if page > len(cursors) { cursors = append(cursors, cursorString) cursorsString += ";" + cursorString } } // Calculate labels: '1'..'9' then 'A'... for i, lbl := len(records)-1, '1'; i >= 0; i-- { if r := records[i]; r.Track() { r.Label = lbl if lbl == '9' { lbl = 'A' - 1 } lbl++ } } p.Custom["CursorList"] = cursors p.Custom["Cursors"] = cursorsString p.Custom["Page"] = page p.Custom["PageSize"] = pageSize p.Custom["RecordOffset"] = (page-1)*pageSize + 1 p.Custom["Records"] = records if p.Mobile { p.Custom["MapWidth"], p.Custom["MapHeight"] = p.Account.GetMobMapPrevSize() p.Custom["MapImgFormat"] = p.Account.GetMobMapImgFormat() } else { p.Custom["MapWidth"], p.Custom["MapHeight"] = p.Account.GetMapPrevSize() } p.Custom["APIKey"] = "AIzaSyCEU_tZ1n0-mMg4woGKIfPqdbi0leSKvjg" p.Custom["AllMarkers"] = allMarkers(records) if len(records) == 0 { if page == 1 { if before.IsZero() && after.IsZero() && areaCode < 0 { p.Custom["PrintNoRecordsForDev"] = true } else { p.Custom["PrintNoMatchForFilters"] = true } } else { p.Custom["PrintNoMoreRecords"] = true } } }
// devices is the logic implementation of the Devices page. func devices(p *page.Params) { c := p.AppCtx fv := p.Request.PostFormValue // Initial values: p.Custom["SearchPrecision"] = 1000 p.Custom["LogsRetention"] = 60 // Detect form submits: switch { case fv("submitAdd") != "": // Add New Device form submitted! // Checks: switch { case !checkName(p, fv("name")): case !checkSearchPrecision(p, fv("searchPrecision")): case !checkLogsRetention(p, fv("logsRetention")): } if p.ErrorMsg == nil { // All data OK, save new Device searchPrecision, _ := strconv.ParseInt(fv("searchPrecision"), 10, 64) logsRetention, _ := strconv.Atoi(fv("logsRetention")) dev := ds.Device{fv("name"), 0, logsRetention, "", time.Now(), 0} dev.SetSearchPrecision(searchPrecision) genNewRandID(p, &dev) if p.Err != nil { return } if _, p.Err = datastore.Put(c, datastore.NewIncompleteKey(c, ds.ENameDevice, p.Account.GetKey(c)), &dev); p.Err != nil { return // Datastore error } p.InfoMsg = "New Device saved successfully." // Clear from memcache: cache.ClearDevListForAccKey(c, p.Account.GetKey(c)) } else { // Submitted values p.Custom["Name"] = fv("name") p.Custom["SearchPrecision"] = fv("searchPrecision") p.Custom["LogsRetention"] = fv("logsRetention") } case fv("submitRename") != "": // Rename Device form submitted! if !checkName(p, fv("name")) { break } if devID, err := strconv.ParseInt(string(fv("devID")), 10, 64); err != nil { p.ErrorMsg = "Invalid Device!" } else { devKey := datastore.NewKey(c, ds.ENameDevice, "", devID, p.Account.GetKey(c)) var dev ds.Device if err = datastore.Get(c, devKey, &dev); err != nil { if err == datastore.ErrNoSuchEntity { p.ErrorMsg = "You do not have access to the specified Device!" } else { // Real datastore error p.Err = err return } } else { // Proceed to rename dev.Name = fv("name") if _, p.Err = datastore.Put(c, devKey, &dev); p.Err != nil { return // Datastore error } p.InfoMsg = "Device renamed successfully." // Clear from memcache: cache.ClearDevListForAccKey(c, p.Account.GetKey(c)) cache.ClearDeviceForRandID(c, dev.RandID) dev.KeyID = devID // This is important (device is loaded freshly and not yet set)! cache.CacheDevice(c, &dev) } } case fv("submitGenNewKey") != "": // Generate New Key form submitted! if devID, err := strconv.ParseInt(string(fv("devID")), 10, 64); err != nil { p.ErrorMsg = "Invalid Device!" } else { devKey := datastore.NewKey(c, ds.ENameDevice, "", devID, p.Account.GetKey(c)) var dev ds.Device if err = datastore.Get(c, devKey, &dev); err != nil { if err == datastore.ErrNoSuchEntity { p.ErrorMsg = "You do not have access to the specified Device!" } else { // Real datastore error p.Err = err return } } else { // Proceed to generate new key // Store old RandID to remove it from cache if saving succeeds oldRandID := dev.RandID genNewRandID(p, &dev) if p.Err != nil { return } if _, p.Err = datastore.Put(c, devKey, &dev); p.Err != nil { return // Datastore error } p.InfoMsg = template.HTML("New Key generated successfully.") p.ImportantMsg = template.HTML("<b>Important!</b> You have to update the URL in the client application else further GPS tracking calls will be discarded!") cache.ClearDeviceForRandID(c, oldRandID) dev.KeyID = devID // This is important (device is loaded freshly and not yet set)! cache.CacheDevice(c, &dev) } } } q := datastore.NewQuery(ds.ENameDevice).Ancestor(p.Account.GetKey(c)).Order(ds.PNameName) var devices []*ds.Device var devKeys []*datastore.Key if devKeys, p.Err = q.GetAll(c, &devices); p.Err != nil { return } for i := range devices { devices[i].KeyID = devKeys[i].IntID() } p.Custom["Devices"] = devices }
// register is the logic implementation of the Register page. func register(p *page.Params) { // Register page is special. Unlike with other pages we do have to check User and Account here // Because even though Register page works with User, logged in user is not required for this page! // User might have logged out on a separate tab if p.User == nil { return } // User might have registered on a different tab if p.Account != nil { return } fv := p.Request.PostFormValue if fv("submitRegister") == "" { // No form submitted. Initial values: p.Custom["GoogleAccount"] = p.User.Email return } p.Custom["GoogleAccount"] = fv("googleAccount") p.Custom["ContactEmail"] = fv("contactEmail") p.Custom["AcceptTermsAndPolicy"] = fv("acceptTermsAndPolicy") // Checks: switch { case !checkGoogleAccounts(p, fv("googleAccount")): case !checkContactEmail(p, fv("contactEmail")): case !checkAcceptTermsAndPolicy(p, fv("acceptTermsAndPolicy")): } // UNTIL PROJECT GOES PUBLIC, DISABLE REGISTRATION: if !appengine.IsDevAppServer() && p.ErrorMsg == nil { p.ErrorMsg = "REGISTRATION IS CURRENTLY DISABLED! Contact the administrator if you would like to register!" return } // END OF: UNTIL PROJECT GOES PUBLIC, DISABLE REGISTRATION if p.ErrorMsg != nil { return } // All data OK, save new Account c := p.AppCtx acc := ds.Account{Email: p.User.Email, Lemail: strings.ToLower(p.User.Email), UserID: p.User.ID, ContactEmail: fv("contactEmail"), Created: time.Now()} var key *datastore.Key if key, p.Err = datastore.Put(c, datastore.NewIncompleteKey(c, ds.ENameAccount, nil), &acc); p.Err == nil { p.Custom["Created"] = true acc.KeyID = key.IntID() p.Account = &acc // Put new Account into the cache cache.CacheAccount(c, p.Account) } // Send registration email (Account info email) const adminEmail = "Andras Belicza <*****@*****.**>" msg := &mail.Message{ Sender: adminEmail, To: []string{acc.Email}, Bcc: []string{adminEmail}, ReplyTo: adminEmail, Subject: "[IczaGPS] Account Info", Body: fmt.Sprintf(accountInfoMail, acc.Email), } if len(acc.ContactEmail) > 0 { msg.Cc = []string{acc.ContactEmail} } if err := mail.Send(c, msg); err == nil { c.Infof("Sent successful registration email.") } else { c.Errorf("Couldn't send email: %v", err) } }
// alerts is the logic implementation of the Alerts page. func alerts(p *page.Params) { c := p.AppCtx // First get devices var devices []*ds.Device if devices, p.Err = cache.GetDevListForAccKey(c, p.Account.GetKey(c)); p.Err != nil { return } p.Custom["Devices"] = devices fv := p.Request.PostFormValue // Detect form submits: switch { case fv("submitAdd") != "": // Add New Alert form submitted! // Checks: switch { case !checkDeviceID(p, fv("carDeviceID"), true, devices): case !checkDeviceID(p, fv("persMobDeviceID"), false, devices): } if p.ErrorMsg == nil { // So far so good. Futher checks: car and personal mobile device must differ carDevID, _ := strconv.ParseInt(fv("carDeviceID"), 10, 64) var persMobDevID int64 if fv("persMobDeviceID") != "" { persMobDevID, _ = strconv.ParseInt(fv("persMobDeviceID"), 10, 64) if carDevID == persMobDevID { p.ErrorMsg = template.HTML(`<span class="code">Car GPS Device</span> and <span class="code">Personal Mobile GPS Device</span> cannot be the same!`) } } if p.ErrorMsg == nil { // So far still good. Furter check: same alert cannot be saved twice q := datastore.NewQuery(ds.ENameAlert).Ancestor(p.Account.GetKey(c)) var alerts []*ds.Alert if _, p.Err = q.GetAll(c, &alerts); p.Err != nil { return } for _, alert := range alerts { if alert.CarDevID == carDevID && alert.PersMobDevID == persMobDevID { p.ErrorMsg = template.HTML(`An Alert with the same <span class="code">Car GPS Device</span> and <span class="code">Personal Mobile GPS Device</span> already exists!`) } } } if p.ErrorMsg == nil { // All data OK, save new Alert alert := ds.Alert{carDevID, persMobDevID, time.Now(), 0, "", ""} if _, p.Err = datastore.Put(c, datastore.NewIncompleteKey(c, ds.ENameAlert, p.Account.GetKey(c)), &alert); p.Err != nil { return // Datastore error } p.InfoMsg = "New Alert saved successfully." } } case fv("submitDelete") != "": // Delete Alert form submitted! if alertID, err := strconv.ParseInt(string(fv("alertID")), 10, 64); err != nil { p.ErrorMsg = "Invalid Alert!" } else { alertKey := datastore.NewKey(c, ds.ENameAlert, "", alertID, p.Account.GetKey(c)) // Check if var alert ds.Alert if err = datastore.Get(c, alertKey, &alert); err != nil { if err == datastore.ErrNoSuchEntity { p.ErrorMsg = "You do not have access to the specified Alert!" } else { // Real datastore error p.Err = err return } } else { // Proceed to delete if p.Err = datastore.Delete(c, alertKey); p.Err != nil { return // Datastore error } p.InfoMsg = "Alert deleted successfully." } } } q := datastore.NewQuery(ds.ENameAlert).Ancestor(p.Account.GetKey(c)) var alerts []*ds.Alert var alertKeys []*datastore.Key if alertKeys, p.Err = q.GetAll(c, &alerts); p.Err != nil { return } for i, alert := range alerts { alert.KeyID = alertKeys[i].IntID() for _, d := range devices { switch d.KeyID { case alert.CarDevID: alert.CarDevName = d.Name case alert.PersMobDevID: alert.PersMobDevName = d.Name } } } p.Custom["Alerts"] = alerts }