aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--auth.go7
-rw-r--r--auth_persona.go69
-rw-r--r--bommom.go1
-rw-r--r--serve.go67
-rw-r--r--templates/account.html29
-rw-r--r--templates/base.html20
-rw-r--r--templates/bom_upload.html4
-rw-r--r--templates/bom_view.html4
-rw-r--r--templates/home.html4
-rw-r--r--templates/static/include.js1255
-rw-r--r--templates/user.html4
11 files changed, 1449 insertions, 15 deletions
diff --git a/auth.go b/auth.go
index f0ef530..80b3088 100644
--- a/auth.go
+++ b/auth.go
@@ -12,6 +12,7 @@ type AuthService interface {
NewAccount(name, pw, email string) error
ChangePassword(name, oldPw, newPw string) error
GetEmail(name string) (string, error)
+ GetUserName(email string) (string, error)
}
// DummyAuth is a "wide-open" implementation of AuthService for development and
@@ -32,5 +33,9 @@ func (da DummyAuth) ChangePassword(name, oldPw, newPw string) error {
}
func (da DummyAuth) GetEmail(name string) (string, error) {
- return "example@bommom.com", nil
+ return "example@localhost", nil
+}
+
+func (da DummyAuth) GetUserName(name string) (string, error) {
+ return "common", nil
}
diff --git a/auth_persona.go b/auth_persona.go
new file mode 100644
index 0000000..7895c2d
--- /dev/null
+++ b/auth_persona.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+)
+
+type PersonaResponse struct {
+ Status, Email, Reason string
+}
+
+func (b PersonaResponse) Okay() bool {
+ return b.Status == "okay"
+}
+
+func VerifyPersonaAssertion(assertion, audience string) PersonaResponse {
+ resp, _ := http.PostForm(
+ "https://browserid.org/verify",
+ url.Values{
+ "assertion": {assertion},
+ "audience": {audience},
+ })
+ response := personaResponseFromJson(resp.Body)
+ resp.Body.Close()
+
+ return response
+}
+
+func personaResponseFromJson(r io.Reader) (resp PersonaResponse) {
+ body, err := ioutil.ReadAll(r)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = json.Unmarshal(body, &resp)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return resp
+}
+
+type PersonaAuth bool
+
+func (pa PersonaAuth) CheckLogin(name, pw string) error {
+ return nil
+}
+
+func (pa PersonaAuth) NewAccount(name, pw, email string) error {
+ return nil
+}
+
+func (pa PersonaAuth) ChangePassword(name, oldPw, newPw string) error {
+ return nil
+}
+
+func (pa PersonaAuth) GetEmail(name string) (string, error) {
+ return "example@localhost", nil
+}
+
+func (pa PersonaAuth) GetUserName(name string) (string, error) {
+ return "common", nil
+}
diff --git a/bommom.go b/bommom.go
index 2d14bf8..55071b8 100644
--- a/bommom.go
+++ b/bommom.go
@@ -27,6 +27,7 @@ var (
inFormat = flag.String("informat", "", "command output format (for 'load' etc)")
listenPort = flag.Uint("port", 7070, "port to listen on (HTTP serve)")
listenHost = flag.String("host", "", "hostname to listen on (HTTP serve)")
+ sessionSecret = flag.String("sessionSecret", "12345", "cookie session secret")
)
func main() {
diff --git a/serve.go b/serve.go
index dedd0a1..d1d035d 100644
--- a/serve.go
+++ b/serve.go
@@ -8,12 +8,15 @@ import (
"regexp"
"path/filepath"
"time"
+ "code.google.com/p/gorilla/sessions"
)
var (
- tmplHome, tmplView, tmplUser, tmplBomView, tmplBomUpload *template.Template
+ tmplHome, tmplView, tmplAccount, tmplUser, tmplBomView, tmplBomUpload *template.Template
)
+var store = sessions.NewCookieStore([]byte(*sessionSecret))
+
func baseHandler(w http.ResponseWriter, r *http.Request) {
var err error
log.Printf("serving %s\n", r.URL.Path)
@@ -25,6 +28,12 @@ func baseHandler(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/":
err = homeController(w, r)
+ case r.URL.Path == "/account/login/":
+ err = loginController(w, r)
+ case r.URL.Path == "/account/logout/":
+ err = logoutController(w, r)
+ //case r.URL.Path == "/account/newuser/":
+ // err = newUserController(w, r)
case bomUploadUrlPattern.MatchString(r.URL.Path):
match := bomUploadUrlPattern.FindStringSubmatch(r.URL.Path)
err = bomUploadController(w, r, match[1], match[2])
@@ -48,7 +57,10 @@ func baseHandler(w http.ResponseWriter, r *http.Request) {
}
func homeController(w http.ResponseWriter, r *http.Request) (err error) {
+ session, _ := store.Get(r, "bommom")
context := make(map[string]interface{})
+ context["Session"] = session.Values
+ log.Printf("%s\n", session.Values["UserName"])
context["BomList"], err = bomstore.ListBoms("")
if err != nil {
return
@@ -57,13 +69,54 @@ func homeController(w http.ResponseWriter, r *http.Request) (err error) {
return
}
+func loginController(w http.ResponseWriter, r *http.Request) (err error) {
+ session, _ := store.Get(r, "bommom")
+ context := make(map[string]interface{})
+ context["ActionLogin"] = true
+ context["Session"] = session.Values
+ if r.Method == "POST" {
+ if isShortName(r.FormValue("UserName")) != true {
+ context["Problem"] = "Ugh, need to use a SHORTNAME!"
+ err = tmplAccount.Execute(w, context)
+ return
+ }
+ audience := "http://localhost:7070"
+ vResponse := VerifyPersonaAssertion(r.FormValue("assertion"), audience)
+ if vResponse.Okay() {
+ session.Values["UserName"] = r.FormValue("UserName")
+ session.Values["Email"] = vResponse.Email
+ session.Save(r, w)
+ context["Session"] = session.Values
+ http.Redirect(w, r, "/", 302)
+ return
+ } else {
+ context["Problem"] = vResponse.Reason
+ err = tmplAccount.Execute(w, context)
+ return
+ }
+ }
+ err = tmplAccount.Execute(w, context)
+ return
+}
+
+func logoutController(w http.ResponseWriter, r *http.Request) (err error) {
+ session, _ := store.Get(r, "bommom")
+ context := make(map[string]interface{})
+ delete(session.Values, "UserName")
+ delete(session.Values, "Email")
+ session.Save(r, w)
+ context["Session"] = session.Values
+ context["ActionLogout"] = true
+ err = tmplAccount.Execute(w, context)
+ return
+}
+
func userController(w http.ResponseWriter, r *http.Request, user, extra string) (err error) {
+ session, _ := store.Get(r, "bommom")
if !isShortName(user) {
http.Error(w, "invalid username: "+user, 400)
return
}
- var email string
- email, err = auth.GetEmail(user)
if err != nil {
// no such user
http.NotFound(w, r)
@@ -72,6 +125,7 @@ func userController(w http.ResponseWriter, r *http.Request, user, extra string)
context := make(map[string]interface{})
context["BomList"], err = bomstore.ListBoms(ShortName(user))
context["UserName"] = user
+ context["Session"] = session.Values
if err != nil {
return
}
@@ -80,6 +134,7 @@ func userController(w http.ResponseWriter, r *http.Request, user, extra string)
}
func bomController(w http.ResponseWriter, r *http.Request, user, name string) (err error) {
+ session, _ := store.Get(r, "bommom")
if !isShortName(user) {
http.Error(w, "invalid username: "+user, 400)
return
@@ -90,6 +145,7 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e
}
context := make(map[string]interface{})
context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name))
+ context["Session"] = session.Values
if err != nil {
http.Error(w, "404 couldn't open bom: " + user + "/" + name, 404)
return nil
@@ -99,6 +155,7 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e
}
func bomUploadController(w http.ResponseWriter, r *http.Request, user, name string) (err error) {
+ session, _ := store.Get(r, "bommom")
if !isShortName(user) {
http.Error(w, "invalid username: "+user, 400)
@@ -109,6 +166,7 @@ func bomUploadController(w http.ResponseWriter, r *http.Request, user, name stri
return
}
context := make(map[string]interface{})
+ context["Session"] = session.Values
context["user"] = ShortName(user)
context["name"] = ShortName(name)
context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name))
@@ -202,6 +260,9 @@ func serveCmd() {
// load and parse templates
baseTmplPath := *templatePath + "/base.html"
tmplHome = template.Must(template.ParseFiles(*templatePath+"/home.html", baseTmplPath))
+ tmplAccount = template.Must(template.ParseFiles(*templatePath+"/account.html", baseTmplPath))
+ //tmplLogout = template.Must(template.ParseFiles(*templatePath+"/logout.html", baseTmplPath))
+ //tmplNewUser = template.Must(template.ParseFiles(*templatePath+"/newuser.html", baseTmplPath))
tmplUser = template.Must(template.ParseFiles(*templatePath+"/user.html", baseTmplPath))
tmplBomView = template.Must(template.ParseFiles(*templatePath+"/bom_view.html", baseTmplPath))
tmplBomUpload = template.Must(template.ParseFiles(*templatePath+"/bom_upload.html", baseTmplPath))
diff --git a/templates/account.html b/templates/account.html
new file mode 100644
index 0000000..9b7e3b1
--- /dev/null
+++ b/templates/account.html
@@ -0,0 +1,29 @@
+{{ template "HEADER" . }}
+{{ if .ActionLogin }}
+
+<br><br><br><br>
+<div class="container">
+ <div class="row">
+ <div class="span6 offset3 well">
+ <legend>Please Sign In</legend>
+ {{ if .Problem }}
+ <div class="alert alert-error">
+ <strong>Problem!</strong> {{ .Problem }}
+ </div>
+ {{ end }}
+ <form method="POST" action="/account/login/" id="login-form">
+ <input id="assertion-field" type="hidden" name="assertion" value="">
+ <input type="text" id="UserName" class="span6" name="UserName" placeholder="Username">
+ </form>
+ <a href="javascript:login()"><button class="btn btn-info btn-block">Sign In With Mozilla Persona</button></a>
+ </div>
+ </div>
+</div>
+
+{{ else }}
+{{if .ActionLogout }}
+<h1>logged out, goodbye!</h1>
+{{ else }}{{if .ActionNewUser }}
+<h1>hello new human!</h1>
+{{end}}{{end}}{{end}}
+{{ template "FOOTER" . }}
diff --git a/templates/base.html b/templates/base.html
index 8066063..4fe66ad 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -2,6 +2,19 @@
<head>
<title>bommom</title>
<link rel="stylesheet" type="text/css" href="/static/default.css"></link>
+<script src="/static/include.js"></script>
+<script>
+function login() {
+ navigator.id.get(function(assertion) {
+ if (assertion) {
+ var assertion_field = document.getElementById("assertion-field");
+ assertion_field.value = assertion;
+ var login_form = document.getElementById("login-form");
+ login_form.submit();
+ }
+ });
+}
+</script>
</head>
<body>
@@ -12,10 +25,11 @@
{{ range .BreadCrumbs }} / {{.}} {{end}}
<div class="nav-collapse collapse">
<p class="navbar-text pull-right">
- {{ if .User }}
- {{ .User.Email }}
+ {{ if .Session.UserName }}
+ {{ .Session.UserName }}
+ <a href="/account/logout/"><button class="btn btn-mini" type="button" style="margin-left: 5px;">Log out!</button></a>
{{ else }}
- <button class="btn btn-mini disabled" type="button" style="margin-top: 9px;">Log in!</button>
+ <a href="/account/login/"><button class="btn btn-mini" type="button" style="margin-top: 9px;">Log in!</button></a>
{{ end }}
</p>
</div><!--/.nav-collapse -->
diff --git a/templates/bom_upload.html b/templates/bom_upload.html
index 30e2ff6..8cc15c8 100644
--- a/templates/bom_upload.html
+++ b/templates/bom_upload.html
@@ -1,4 +1,4 @@
-{{ template "HEADER" }}
+{{ template "HEADER" . }}
{{ if .BomMeta }}
<h1>{{ .BomMeta.Name }} is a bom.</h1>
@@ -21,4 +21,4 @@
<input type="submit" value="Upload"></input>
</form>
-{{ template "FOOTER" }}
+{{ template "FOOTER" . }}
diff --git a/templates/bom_view.html b/templates/bom_view.html
index 2e66bf1..7eca3e3 100644
--- a/templates/bom_view.html
+++ b/templates/bom_view.html
@@ -1,4 +1,4 @@
-{{ template "HEADER" }}
+{{ template "HEADER" . }}
<h1>{{ .BomMeta.Name }} is a bom.</h1>
<a href="./_upload/"><button class="btn btn-mini">upload new</button></a>
<br>
@@ -32,4 +32,4 @@
</tr>
{{ end }}
</table>
-{{ template "FOOTER" }}
+{{ template "FOOTER" . }}
diff --git a/templates/home.html b/templates/home.html
index 8749c58..40e8f6c 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -1,4 +1,4 @@
-{{ template "HEADER" }}
+{{ template "HEADER" . }}
<h1>this is bomom.</h1>
<table class="table">
{{ range .BomList }}
@@ -8,4 +8,4 @@
No Boms found!
{{ end }}
</table>
-{{ template "FOOTER" }}
+{{ template "FOOTER" . }}
diff --git a/templates/static/include.js b/templates/static/include.js
new file mode 100644
index 0000000..1df41c0
--- /dev/null
+++ b/templates/static/include.js
@@ -0,0 +1,1255 @@
+/**
+ * Uncompressed source can be found at https://login.persona.org/include.orig.js
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+(function() {
+ // this is the file that the RP includes to shim in the
+ // navigator.id.getVerifiedEmail() function
+ // "use strict";
+
+ // local embedded copy of jschannel: http://github.com/mozilla/jschannel
+ /**
+ * js_channel is a very lightweight abstraction on top of
+ * postMessage which defines message formats and semantics
+ * to support interactions more rich than just message passing
+ * js_channel supports:
+ * + query/response - traditional rpc
+ * + query/update/response - incremental async return of results
+ * to a query
+ * + notifications - fire and forget
+ * + error handling
+ *
+ * js_channel is based heavily on json-rpc, but is focused at the
+ * problem of inter-iframe RPC.
+ *
+ * Message types:
+ * There are 5 types of messages that can flow over this channel,
+ * and you may determine what type of message an object is by
+ * examining its parameters:
+ * 1. Requests
+ * + integer id
+ * + string method
+ * + (optional) any params
+ * 2. Callback Invocations (or just "Callbacks")
+ * + integer id
+ * + string callback
+ * + (optional) params
+ * 3. Error Responses (or just "Errors)
+ * + integer id
+ * + string error
+ * + (optional) string message
+ * 4. Responses
+ * + integer id
+ * + (optional) any result
+ * 5. Notifications
+ * + string method
+ * + (optional) any params
+ */
+ var Channel = (function() {
+ "use strict";
+
+ // current transaction id, start out at a random *odd* number between 1 and a million
+ // There is one current transaction counter id per page, and it's shared between
+ // channel instances. That means of all messages posted from a single javascript
+ // evaluation context, we'll never have two with the same id.
+ var s_curTranId = Math.floor(Math.random()*1000001);
+
+ // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window.
+ // futher if two bound channels have the same window and scope, they may not have *overlapping* origins
+ // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
+ // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
+ // handlers. Request and Notification messages are routed using this table.
+ // Finally, channels are inserted into this table when built, and removed when destroyed.
+ var s_boundChans = { };
+
+ // add a channel to s_boundChans, throwing if a dup exists
+ function s_addBoundChan(win, origin, scope, handler) {
+ function hasWin(arr) {
+ for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true;
+ return false;
+ }
+
+ // does she exist?
+ var exists = false;
+
+
+ if (origin === '*') {
+ // we must check all other origins, sadly.
+ for (var k in s_boundChans) {
+ if (!s_boundChans.hasOwnProperty(k)) continue;
+ if (k === '*') continue;
+ if (typeof s_boundChans[k][scope] === 'object') {
+ exists = hasWin(s_boundChans[k][scope]);
+ if (exists) break;
+ }
+ }
+ } else {
+ // we must check only '*'
+ if ((s_boundChans['*'] && s_boundChans['*'][scope])) {
+ exists = hasWin(s_boundChans['*'][scope]);
+ }
+ if (!exists && s_boundChans[origin] && s_boundChans[origin][scope])
+ {
+ exists = hasWin(s_boundChans[origin][scope]);
+ }
+ }
+ if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'";
+
+ if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
+ if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ];
+ s_boundChans[origin][scope].push({win: win, handler: handler});
+ }
+
+ function s_removeBoundChan(win, origin, scope) {
+ var arr = s_boundChans[origin][scope];
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].win === win) {
+ arr.splice(i,1);
+ }
+ }
+ if (s_boundChans[origin][scope].length === 0) {
+ delete s_boundChans[origin][scope];
+ }
+ }
+
+ function s_isArray(obj) {
+ if (Array.isArray) return Array.isArray(obj);
+ else {
+ return (obj.constructor.toString().indexOf("Array") != -1);
+ }
+ }
+
+ // No two outstanding outbound messages may have the same id, period. Given that, a single table
+ // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
+ // Response messages. Entries are added to this table when requests are sent, and removed when
+ // responses are received.
+ var s_transIds = { };
+
+ // class singleton onMessage handler
+ // this function is registered once and all incoming messages route through here. This
+ // arrangement allows certain efficiencies, message data is only parsed once and dispatch
+ // is more efficient, especially for large numbers of simultaneous channels.
+ var s_onMessage = function(e) {
+ try {
+ var m = JSON.parse(e.data);
+ if (typeof m !== 'object' || m === null) throw "malformed";
+ } catch(e) {
+ // just ignore any posted messages that do not consist of valid JSON
+ return;
+ }
+
+ var w = e.source;
+ var o = e.origin;
+ var s, i, meth;
+
+ if (typeof m.method === 'string') {
+ var ar = m.method.split('::');
+ if (ar.length == 2) {
+ s = ar[0];
+ meth = ar[1];
+ } else {
+ meth = m.method;
+ }
+ }
+
+ if (typeof m.id !== 'undefined') i = m.id;
+
+ // w is message source window
+ // o is message origin
+ // m is parsed message
+ // s is message scope
+ // i is message id (or undefined)
+ // meth is unscoped method name
+ // ^^ based on these factors we can route the message
+
+ // if it has a method it's either a notification or a request,
+ // route using s_boundChans
+ if (typeof meth === 'string') {
+ var delivered = false;
+ if (s_boundChans[o] && s_boundChans[o][s]) {
+ for (var j = 0; j < s_boundChans[o][s].length; j++) {
+ if (s_boundChans[o][s][j].win === w) {
+ s_boundChans[o][s][j].handler(o, meth, m);
+ delivered = true;
+ break;
+ }
+ }
+ }
+
+ if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) {
+ for (var j = 0; j < s_boundChans['*'][s].length; j++) {
+ if (s_boundChans['*'][s][j].win === w) {
+ s_boundChans['*'][s][j].handler(o, meth, m);
+ break;
+ }
+ }
+ }
+ }
+ // otherwise it must have an id (or be poorly formed
+ else if (typeof i != 'undefined') {
+ if (s_transIds[i]) s_transIds[i](o, meth, m);
+ }
+ };
+
+ // Setup postMessage event listeners
+ if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
+ else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage);
+
+ /* a messaging channel is constructed from a window and an origin.
+ * the channel will assert that all messages received over the
+ * channel match the origin
+ *
+ * Arguments to Channel.build(cfg):
+ *
+ * cfg.window - the remote window with which we'll communicate
+ * cfg.origin - the expected origin of the remote window, may be '*'
+ * which matches any origin
+ * cfg.scope - the 'scope' of messages. a scope string that is
+ * prepended to message names. local and remote endpoints
+ * of a single channel must agree upon scope. Scope may
+ * not contain double colons ('::').
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.postMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these immediately
+ * before messages are posted.
+ * cfg.gotMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these arguments
+ * immediately after they pass scope and origin checks, but before
+ * they are processed.
+ * cfg.onReady - A function that will be invoked when a channel becomes "ready",
+ * this occurs once both sides of the channel have been
+ * instantiated and an application level handshake is exchanged.
+ * the onReady function will be passed a single argument which is
+ * the channel object that was returned from build().
+ */
+ return {
+ build: function(cfg) {
+ var debug = function(m) {
+ if (cfg.debugOutput && window.console && window.console.log) {
+ // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
+ try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { }
+ console.log("["+chanId+"] " + m);
+ }
+ };
+
+ /* browser capabilities check */
+ if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage");
+ if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
+ throw("jschannel cannot run this browser, no JSON parsing/serialization");
+ }
+
+ /* basic argument validation */
+ if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument");
+
+ if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument");
+
+ /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
+ * window... Not sure if we care to support that */
+ if (window === cfg.window) throw("target window is same as present window -- not allowed");
+
+ // let's require that the client specify an origin. if we just assume '*' we'll be
+ // propagating unsafe practices. that would be lame.
+ var validOrigin = false;
+ if (typeof cfg.origin === 'string') {
+ var oMatch;
+ if (cfg.origin === "*") validOrigin = true;
+ // allow valid domains under http and https. Also, trim paths off otherwise valid origins.
+ else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) {
+ cfg.origin = oMatch[0].toLowerCase();
+ validOrigin = true;
+ }
+ }
+
+ if (!validOrigin) throw ("Channel.build() called with an invalid origin");
+
+ if (typeof cfg.scope !== 'undefined') {
+ if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
+ if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'";
+ }
+
+ /* private variables */
+ // generate a random and psuedo unique id for this channel
+ var chanId = (function () {
+ var text = "";
+ var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
+ return text;
+ })();
+
+ // registrations: mapping method names to call objects
+ var regTbl = { };
+ // current oustanding sent requests
+ var outTbl = { };
+ // current oustanding received requests
+ var inTbl = { };
+ // are we ready yet? when false we will block outbound messages.
+ var ready = false;
+ var pendingQueue = [ ];
+
+ var createTransaction = function(id,origin,callbacks) {
+ var shouldDelayReturn = false;
+ var completed = false;
+
+ return {
+ origin: origin,
+ invoke: function(cbName, v) {
+ // verify in table
+ if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id;
+ // verify that the callback name is valid
+ var valid = false;
+ for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
+ if (!valid) throw "request supports no such callback '" + cbName + "'";
+
+ // send callback invocation
+ postMessage({ id: id, callback: cbName, params: v});
+ },
+ error: function(error, message) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "error called for nonexistent message: " + id;
+
+ // remove transaction from table
+ delete inTbl[id];
+
+ // send error
+ postMessage({ id: id, error: error, message: message });
+ },
+ complete: function(v) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "complete called for nonexistent message: " + id;
+ // remove transaction from table
+ delete inTbl[id];
+ // send complete
+ postMessage({ id: id, result: v });
+ },
+ delayReturn: function(delay) {
+ if (typeof delay === 'boolean') {
+ shouldDelayReturn = (delay === true);
+ }
+ return shouldDelayReturn;
+ },
+ completed: function() {
+ return completed;
+ }
+ };
+ };
+
+ var setTransactionTimeout = function(transId, timeout, method) {
+ return window.setTimeout(function() {
+ if (outTbl[transId]) {
+ // XXX: what if client code raises an exception here?
+ var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'";
+ (1,outTbl[transId].error)("timeout_error", msg);
+ delete outTbl[transId];
+ delete s_transIds[transId];
+ }
+ }, timeout);
+ };
+
+ var onMessage = function(origin, method, m) {
+ // if an observer was specified at allocation time, invoke it
+ if (typeof cfg.gotMessageObserver === 'function') {
+ // pass observer a clone of the object so that our
+ // manipulations are not visible (i.e. method unscoping).
+ // This is not particularly efficient, but then we expect
+ // that message observers are primarily for debugging anyway.
+ try {
+ cfg.gotMessageObserver(origin, m);
+ } catch (e) {
+ debug("gotMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ // now, what type of message is this?
+ if (m.id && method) {
+ // a request! do we have a registered handler for this request?
+ if (regTbl[method]) {
+ var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]);
+ inTbl[m.id] = { };
+ try {
+ // callback handling. we'll magically create functions inside the parameter list for each
+ // callback
+ if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) {
+ for (var i = 0; i < m.callbacks.length; i++) {
+ var path = m.callbacks[i];
+ var obj = m.params;
+ var pathItems = path.split('/');
+ for (var j = 0; j < pathItems.length - 1; j++) {
+ var cp = pathItems[j];
+ if (typeof obj[cp] !== 'object') obj[cp] = { };
+ obj = obj[cp];
+ }
+ obj[pathItems[pathItems.length - 1]] = (function() {
+ var cbName = path;
+ return function(params) {
+ return trans.invoke(cbName, params);
+ };
+ })();
+ }
+ }
+ var resp = regTbl[method](trans, m.params);
+ if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
+ } catch(e) {
+ // automagic handling of exceptions:
+ var error = "runtime_error";
+ var message = null;
+ // * if it's a string then it gets an error code of 'runtime_error' and string is the message
+ if (typeof e === 'string') {
+ message = e;
+ } else if (typeof e === 'object') {
+ // either an array or an object
+ // * if it's an array of length two, then array[0] is the code, array[1] is the error message
+ if (e && s_isArray(e) && e.length == 2) {
+ error = e[0];
+ message = e[1];
+ }
+ // * if it's an object then we'll look form error and message parameters
+ else if (typeof e.error === 'string') {
+ error = e.error;
+ if (!e.message) message = "";
+ else if (typeof e.message === 'string') message = e.message;
+ else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
+ }
+ }
+
+ // message is *still* null, let's try harder
+ if (message === null) {
+ try {
+ message = JSON.stringify(e);
+ /* On MSIE8, this can result in 'out of memory', which
+ * leaves message undefined. */
+ if (typeof(message) == 'undefined')
+ message = e.toString();
+ } catch (e2) {
+ message = e.toString();
+ }
+ }
+
+ trans.error(error,message);
+ }
+ }
+ } else if (m.id && m.callback) {
+ if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
+ {
+ debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")");
+ } else {
+ // XXX: what if client code raises an exception here?
+ outTbl[m.id].callbacks[m.callback](m.params);
+ }
+ } else if (m.id) {
+ if (!outTbl[m.id]) {
+ debug("ignoring invalid response: " + m.id);
+ } else {
+ // XXX: what if client code raises an exception here?
+ if (m.error) {
+ (1,outTbl[m.id].error)(m.error, m.message);
+ } else {
+ if (m.result !== undefined) (1,outTbl[m.id].success)(m.result);
+ else (1,outTbl[m.id].success)();
+ }
+ delete outTbl[m.id];
+ delete s_transIds[m.id];
+ }
+ } else if (method) {
+ // tis a notification.
+ if (regTbl[method]) {
+ // yep, there's a handler for that.
+ // transaction is null for notifications.
+ regTbl[method](null, m.params);
+ // if the client throws, we'll just let it bubble out
+ // what can we do? Also, here we'll ignore return values
+ }
+ }
+ };
+
+ // now register our bound channel for msg routing
+ s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
+
+ // scope method names based on cfg.scope specified when the Channel was instantiated
+ var scopeMethod = function(m) {
+ if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
+ return m;
+ };
+
+ // a small wrapper around postmessage whose primary function is to handle the
+ // case that clients start sending messages before the other end is "ready"
+ var postMessage = function(msg, force) {
+ if (!msg) throw "postMessage called with null message";
+
+ // delay posting if we're not ready yet.
+ var verb = (ready ? "post " : "queue ");
+ debug(verb + " message: " + JSON.stringify(msg));
+ if (!force && !ready) {
+ pendingQueue.push(msg);
+ } else {
+ if (typeof cfg.postMessageObserver === 'function') {
+ try {
+ cfg.postMessageObserver(cfg.origin, msg);
+ } catch (e) {
+ debug("postMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
+ }
+ };
+
+ var onReady = function(trans, type) {
+ debug('ready msg received');
+ if (ready) throw "received ready message while in ready state. help!";
+
+ if (type === 'ping') {
+ chanId += '-R';
+ } else {
+ chanId += '-L';
+ }
+
+ obj.unbind('__ready'); // now this handler isn't needed any more.
+ ready = true;
+ debug('ready msg accepted.');
+
+ if (type === 'ping') {
+ obj.notify({ method: '__ready', params: 'pong' });
+ }
+
+ // flush queue
+ while (pendingQueue.length) {
+ postMessage(pendingQueue.pop());
+ }
+
+ // invoke onReady observer if provided
+ if (typeof cfg.onReady === 'function') cfg.onReady(obj);
+ };
+
+ var obj = {
+ // tries to unbind a bound message handler. returns false if not possible
+ unbind: function (method) {
+ if (regTbl[method]) {
+ if (!(delete regTbl[method])) throw ("can't delete method: " + method);
+ return true;
+ }
+ return false;
+ },
+ bind: function (method, cb) {
+ if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
+ if (!cb || typeof cb !== 'function') throw "callback missing from bind params";
+
+ if (regTbl[method]) throw "method '"+method+"' is already bound!";
+ regTbl[method] = cb;
+ return this;
+ },
+ call: function(m) {
+ if (!m) throw 'missing arguments to call function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
+ if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
+
+ // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
+ // object and pick out all of the functions that were passed as arguments.
+ var callbacks = { };
+ var callbackNames = [ ];
+
+ var pruneFunctions = function (path, obj) {
+ if (typeof obj === 'object') {
+ for (var k in obj) {
+ if (!obj.hasOwnProperty(k)) continue;
+ var np = path + (path.length ? '/' : '') + k;
+ if (typeof obj[k] === 'function') {
+ callbacks[np] = obj[k];
+ callbackNames.push(np);
+ delete obj[k];
+ } else if (typeof obj[k] === 'object') {
+ pruneFunctions(np, obj[k]);
+ }
+ }
+ }
+ };
+ pruneFunctions("", m.params);
+
+ // build a 'request' message and send it
+ var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params };
+ if (callbackNames.length) msg.callbacks = callbackNames;
+
+ if (m.timeout)
+ // XXX: This function returns a timeout ID, but we don't do anything with it.
+ // We might want to keep track of it so we can cancel it using clearTimeout()
+ // when the transaction completes.
+ setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method));
+
+ // insert into the transaction table
+ outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success };
+ s_transIds[s_curTranId] = onMessage;
+
+ // increment current id
+ s_curTranId++;
+
+ postMessage(msg);
+ },
+ notify: function(m) {
+ if (!m) throw 'missing arguments to notify function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
+
+ // no need to go into any transaction table
+ postMessage({ method: scopeMethod(m.method), params: m.params });
+ },
+ destroy: function () {
+ s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
+ if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
+ else if(window.detachEvent) window.detachEvent('onmessage', onMessage);
+ ready = false;
+ regTbl = { };
+ inTbl = { };
+ outTbl = { };
+ cfg.origin = null;
+ pendingQueue = [ ];
+ debug("channel destroyed");
+ chanId = "";
+ }
+ };
+
+ obj.bind('__ready', onReady);
+ setTimeout(function() {
+// postMessage({ method: scopeMethod('__ready'), params: "ping" }, true);
+ }, 0);
+
+ return obj;
+ }
+ };
+ })();
+
+ // local embedded copy of winchan: http://github.com/lloyd/winchan
+ // BEGIN WINCHAN
+
+ ;WinChan = (function() {
+ var RELAY_FRAME_NAME = "__winchan_relay_frame";
+ var CLOSE_CMD = "die";
+
+ // a portable addListener implementation
+ function addListener(w, event, cb) {
+ if(w.attachEvent) w.attachEvent('on' + event, cb);
+ else if (w.addEventListener) w.addEventListener(event, cb, false);
+ }
+
+ // a portable removeListener implementation
+ function removeListener(w, event, cb) {
+ if(w.detachEvent) w.detachEvent('on' + event, cb);
+ else if (w.removeEventListener) w.removeEventListener(event, cb, false);
+ }
+
+ // checking for IE8 or above
+ function isInternetExplorer() {
+ var rv = -1; // Return value assumes failure.
+ if (navigator.appName === 'Microsoft Internet Explorer') {
+ var ua = navigator.userAgent;
+ var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+ if (re.exec(ua) != null)
+ rv = parseFloat(RegExp.$1);
+ }
+ return rv >= 8;
+ }
+
+ // checking Mobile Firefox (Fennec)
+ function isFennec() {
+ try {
+ // We must check for both XUL and Java versions of Fennec. Both have
+ // distinct UA strings.
+ var userAgent = navigator.userAgent;
+ return (userAgent.indexOf('Fennec/') != -1) || // XUL
+ (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java
+ } catch(e) {};
+ return false;
+ }
+
+ // feature checking to see if this platform is supported at all
+ function isSupported() {
+ return (window.JSON && window.JSON.stringify &&
+ window.JSON.parse && window.postMessage);
+ }
+
+ // given a URL, extract the origin
+ function extractOrigin(url) {
+ if (!/^https?:\/\//.test(url)) url = window.location.href;
+ var m = /^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(url);
+ if (m) return m[1];
+ return url;
+ }
+
+ // find the relay iframe in the opener
+ function findRelay() {
+ var loc = window.location;
+ var frames = window.opener.frames;
+ var origin = loc.protocol + '//' + loc.host;
+ for (var i = frames.length - 1; i >= 0; i--) {
+ try {
+ if (frames[i].location.href.indexOf(origin) === 0 &&
+ frames[i].name === RELAY_FRAME_NAME)
+ {
+ return frames[i];
+ }
+ } catch(e) { }
+ }
+ return;
+ }
+
+ var isIE = isInternetExplorer();
+
+ if (isSupported()) {
+ /* General flow:
+ * 0. user clicks
+ * (IE SPECIFIC) 1. caller adds relay iframe (served from trusted domain) to DOM
+ * 2. caller opens window (with content from trusted domain)
+ * 3. window on opening adds a listener to 'message'
+ * (IE SPECIFIC) 4. window on opening finds iframe
+ * 5. window checks if iframe is "loaded" - has a 'doPost' function yet
+ * (IE SPECIFIC5) 5a. if iframe.doPost exists, window uses it to send ready event to caller
+ * (IE SPECIFIC5) 5b. if iframe.doPost doesn't exist, window waits for frame ready
+ * (IE SPECIFIC5) 5bi. once ready, window calls iframe.doPost to send ready event
+ * 6. caller upon reciept of 'ready', sends args
+ */
+ return {
+ open: function(opts, cb) {
+ if (!cb) throw "missing required callback argument";
+
+ // test required options
+ var err;
+ if (!opts.url) err = "missing required 'url' parameter";
+ if (!opts.relay_url) err = "missing required 'relay_url' parameter";
+ if (err) setTimeout(function() { cb(err); }, 0);
+
+ // supply default options
+ if (!opts.window_name) opts.window_name = null;
+ if (!opts.window_features || isFennec()) opts.window_features = undefined;
+
+ // opts.params may be undefined
+
+ var iframe;
+
+ // sanity check, are url and relay_url the same origin?
+ var origin = extractOrigin(opts.url);
+ if (origin !== extractOrigin(opts.relay_url)) {
+ return setTimeout(function() {
+ cb('invalid arguments: origin of url and relay_url must match');
+ }, 0);
+ }
+
+ var messageTarget;
+
+ if (isIE) {
+ // first we need to add a "relay" iframe to the document that's served
+ // from the target domain. We can postmessage into a iframe, but not a
+ // window
+ iframe = document.createElement("iframe");
+ // iframe.setAttribute('name', framename);
+ iframe.setAttribute('src', opts.relay_url);
+ iframe.style.display = "none";
+ iframe.setAttribute('name', RELAY_FRAME_NAME);
+ document.body.appendChild(iframe);
+ messageTarget = iframe.contentWindow;
+ }
+
+ var w = window.open(opts.url, opts.window_name, opts.window_features);
+
+ if (!messageTarget) messageTarget = w;
+
+ var req = JSON.stringify({a: 'request', d: opts.params});
+
+ // cleanup on unload
+ function cleanup() {
+ if (iframe) document.body.removeChild(iframe);
+ iframe = undefined;
+ if (w) {
+ try {
+ w.close();
+ } catch (securityViolation) {
+ // This happens in Opera 12 sometimes
+ // see https://github.com/mozilla/browserid/issues/1844
+ messageTarget.postMessage(CLOSE_CMD, origin);
+ }
+ }
+ w = messageTarget = undefined;
+ }
+
+ addListener(window, 'unload', cleanup);
+
+ function onMessage(e) {
+ try {
+ var d = JSON.parse(e.data);
+ if (d.a === 'ready') messageTarget.postMessage(req, origin);
+ else if (d.a === 'error') {
+ if (cb) {
+ cb(d.d);
+ cb = null;
+ }
+ } else if (d.a === 'response') {
+ removeListener(window, 'message', onMessage);
+ removeListener(window, 'unload', cleanup);
+ cleanup();
+ if (cb) {
+ cb(null, d.d);
+ cb = null;
+ }
+ }
+ } catch(err) { }
+ }
+
+ addListener(window, 'message', onMessage);
+
+ return {
+ close: cleanup,
+ focus: function() {
+ if (w) {
+ try {
+ w.focus();
+ } catch (e) {
+ // IE7 blows up here, do nothing
+ }
+ }
+ }
+ };
+ }
+ };
+ } else {
+ return {
+ open: function(url, winopts, arg, cb) {
+ setTimeout(function() { cb("unsupported browser"); }, 0);
+ }
+ };
+ }
+ })();
+
+
+
+ // END WINCHAN
+
+ var BrowserSupport = (function() {
+ var win = window,
+ nav = navigator,
+ reason;
+
+ // For unit testing
+ function setTestEnv(newNav, newWindow) {
+ nav = newNav;
+ win = newWindow;
+ }
+
+ function getInternetExplorerVersion() {
+ var rv = -1; // Return value assumes failure.
+ if (nav.appName == 'Microsoft Internet Explorer') {
+ var ua = nav.userAgent;
+ var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+ if (re.exec(ua) != null)
+ rv = parseFloat(RegExp.$1);
+ }
+
+ return rv;
+ }
+
+ function checkIE() {
+ var ieVersion = getInternetExplorerVersion(),
+ ieNosupport = ieVersion > -1 && ieVersion < 8;
+
+ if(ieNosupport) {
+ return "BAD_IE_VERSION";
+ }
+ }
+
+ function explicitNosupport() {
+ return checkIE();
+ }
+
+ function checkLocalStorage() {
+ // Firefox/Fennec/Chrome blow up when trying to access or
+ // write to localStorage. We must do two explicit checks, first
+ // whether the browser has localStorage. Second, we must check
+ // whether the localStorage can be written to. Firefox (at v11)
+ // throws an exception when querying win['localStorage']
+ // when cookies are disabled. Chrome (v17) excepts when trying to
+ // write to localStorage when cookies are disabled. If an
+ // exception is thrown, then localStorage is disabled. If no
+ // exception is thrown, hasLocalStorage will be true if the
+ // browser supports localStorage and it can be written to.
+ try {
+ var hasLocalStorage = 'localStorage' in win
+ // Firefox will except here if cookies are disabled.
+ && win['localStorage'] !== null;
+
+ if(hasLocalStorage) {
+ // browser has localStorage, check if it can be written to. If
+ // cookies are disabled, some browsers (Chrome) will except here.
+ win['localStorage'].setItem("test", "true");
+ win['localStorage'].removeItem("test");
+ }
+ else {
+ // Browser does not have local storage.
+ return "LOCALSTORAGE_NOT_SUPPORTED";
+ }
+ } catch(e) {
+ return "LOCALSTORAGE_DISABLED";
+ }
+ }
+
+ function checkPostMessage() {
+ if(!win.postMessage) {
+ return "POSTMESSAGE_NOT_SUPPORTED";
+ }
+ }
+
+ function checkJSON() {
+ if(!(window.JSON && window.JSON.stringify && window.JSON.parse)) {
+ return "JSON_NOT_SUPPORTED";
+ }
+ }
+
+ function isSupported() {
+ reason = explicitNosupport() || checkLocalStorage() || checkPostMessage() || checkJSON();
+
+ return !reason;
+ }
+
+
+ function getNoSupportReason() {
+ return reason;
+ }
+
+ return {
+ /**
+ * Set the test environment.
+ * @method setTestEnv
+ */
+ setTestEnv: setTestEnv,
+ /**
+ * Check whether the current browser is supported
+ * @method isSupported
+ * @returns {boolean}
+ */
+ isSupported: isSupported,
+ /**
+ * Called after isSupported, if isSupported returns false. Gets the reason
+ * why browser is not supported.
+ * @method getNoSupportReason
+ * @returns {string}
+ */
+ getNoSupportReason: getNoSupportReason
+ };
+ }());
+
+ if (!navigator.id) {
+ navigator.id = {};
+ }
+
+ if (!navigator.id.request || navigator.id._shimmed) {
+ var ipServer = "https://login.persona.org";
+ var userAgent = navigator.userAgent;
+ // We must check for both XUL and Java versions of Fennec. Both have
+ // distinct UA strings.
+ var isFennec = (userAgent.indexOf('Fennec/') != -1) || // XUL
+ (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java
+
+ var windowOpenOpts =
+ (isFennec ? undefined :
+ "menubar=0,location=1,resizable=1,scrollbars=1,status=0,dialog=1,minimizable=1,width=700,height=375");
+
+ var w;
+
+ // table of registered observers
+ var observers = {
+ login: null,
+ logout: null,
+ ready: null
+ };
+
+ var compatMode = undefined;
+ function checkCompat(requiredMode) {
+ if (requiredMode === true) {
+ // this deprecation warning should be re-enabled when the .watch and .request APIs become final.
+ // try { console.log("this site uses deprecated APIs (see documentation for navigator.id.request())"); } catch(e) { }
+ }
+
+ if (compatMode === undefined) compatMode = requiredMode;
+ else if (compatMode != requiredMode) {
+ throw "you cannot combine the navigator.id.watch() API with navigator.id.getVerifiedEmail() or navigator.id.get()" +
+ "this site should instead use navigator.id.request() and navigator.id.watch()";
+ }
+ }
+
+ var commChan,
+ browserSupported = BrowserSupport.isSupported();
+
+ // this is for calls that are non-interactive
+ function _open_hidden_iframe() {
+ // If this is an unsupported browser, do not even attempt to add the
+ // IFRAME as doing so will cause an exception to be thrown in IE6 and IE7
+ // from within the communication_iframe.
+ if(!browserSupported) return;
+
+ try {
+ if (!commChan) {
+ var doc = window.document;
+ var iframe = doc.createElement("iframe");
+ iframe.style.display = "none";
+ doc.body.appendChild(iframe);
+ iframe.src = ipServer + "/communication_iframe";
+ commChan = Channel.build({
+ window: iframe.contentWindow,
+ origin: ipServer,
+ scope: "mozid_ni",
+ onReady: function() {
+ // once the channel is set up, we'll fire a loaded message. this is the
+ // cutoff point where we'll say if 'setLoggedInUser' was not called before
+ // this point, then it wont be called (XXX: optimize and improve me)
+ commChan.call({
+ method: 'loaded',
+ success: function(){
+ if (observers.ready) observers.ready();
+ }, error: function() {
+ }
+ });
+ }
+ });
+
+ commChan.bind('logout', function(trans, params) {
+ if (observers.logout) observers.logout();
+ });
+
+ commChan.bind('login', function(trans, params) {
+ if (observers.login) observers.login(params);
+ });
+ }
+ } catch(e) {
+ // channel building failed! let's ignore the error and allow higher
+ // level code to handle user messaging.
+ commChan = undefined;
+ }
+ }
+
+ function defined(item) {
+ return typeof item !== "undefined";
+ }
+
+ function warn(message) {
+ try {
+ console.warn(message);
+ } catch(e) {
+ /* ignore error */
+ }
+ }
+
+ function checkDeprecated(options, field) {
+ if(defined(options[field])) {
+ warn(field + " has been deprecated");
+ return true;
+ }
+ }
+
+ function checkRenamed(options, oldName, newName) {
+ if (defined(options[oldName]) &&
+ defined(options[newName])) {
+ throw "you cannot supply *both* " + oldName + " and " + newName;
+ }
+ else if(checkDeprecated(options, oldName)) {
+ options[newName] = options[oldName];
+ delete options[oldName];
+ }
+ }
+
+ function internalWatch(options) {
+ if (typeof options !== 'object') return;
+
+ if (options.onlogin && typeof options.onlogin !== 'function' ||
+ options.onlogout && typeof options.onlogout !== 'function' ||
+ options.onready && typeof options.onready !== 'function')
+ {
+ throw "non-function where function expected in parameters to navigator.id.watch()";
+ }
+
+ if (!options.onlogin) throw "'onlogin' is a required argument to navigator.id.watch()";
+ if (!options.onlogout) throw "'onlogout' is a required argument to navigator.id.watch()";
+
+ observers.login = options.onlogin || null;
+ observers.logout = options.onlogout || null;
+ observers.ready = options.onready || null;
+
+ _open_hidden_iframe();
+
+ // back compat support for loggedInEmail
+ checkRenamed(options, "loggedInEmail", "loggedInUser");
+
+ // check that the commChan was properly initialized before interacting with it.
+ // on unsupported browsers commChan might still be undefined, in which case
+ // we let the dialog display the "unsupported browser" message upon spawning.
+ if (typeof options.loggedInUser !== 'undefined' && commChan) {
+ commChan.notify({
+ method: 'loggedInUser',
+ params: options.loggedInUser
+ });
+ }
+ }
+
+ function internalRequest(options) {
+ checkDeprecated(options, "requiredEmail");
+ checkRenamed(options, "tosURL", "termsOfService");
+ checkRenamed(options, "privacyURL", "privacyPolicy");
+
+ if (options.termsOfService && !options.privacyPolicy) {
+ warn("termsOfService ignored unless privacyPolicy also defined");
+ }
+
+ if (options.privacyPolicy && !options.termsOfService) {
+ warn("privacyPolicy ignored unless termsOfService also defined");
+ }
+
+ // focus an existing window
+ if (w) {
+ try {
+ w.focus();
+ }
+ catch(e) {
+ /* IE7 blows up here, do nothing */
+ }
+ return;
+ }
+
+ if (!BrowserSupport.isSupported()) {
+ var reason = BrowserSupport.getNoSupportReason(),
+ url = "unsupported_dialog";
+
+ if(reason === "LOCALSTORAGE_DISABLED") {
+ url = "cookies_disabled";
+ }
+
+ w = window.open(
+ ipServer + "/" + url,
+ null,
+ windowOpenOpts);
+ return;
+ }
+
+ // notify the iframe that the dialog is running so we
+ // don't do duplicative work
+ if (commChan) commChan.notify({ method: 'dialog_running' });
+
+ w = WinChan.open({
+ url: ipServer + '/sign_in',
+ relay_url: ipServer + '/relay',
+ window_features: windowOpenOpts,
+ window_name: '__persona_dialog',
+ params: {
+ method: "get",
+ params: options
+ }
+ }, function(err, r) {
+ // unpause the iframe to detect future changes in login state
+ if (commChan) {
+ // update the loggedInUser in the case that an assertion was generated, as
+ // this will prevent the comm iframe from thinking that state has changed
+ // and generating a new assertion. IF, however, this request is not a success,
+ // then we do not change the loggedInUser - and we will let the comm frame determine
+ // if generating a logout event is the right thing to do
+ if (!err && r && r.email) {
+ commChan.notify({ method: 'loggedInUser', params: r.email });
+ }
+ commChan.notify({ method: 'dialog_complete' });
+ }
+
+ // clear the window handle
+ w = undefined;
+ if (!err && r && r.assertion) {
+ try {
+ if (observers.login) observers.login(r.assertion);
+ } catch(e) {
+ // client's observer threw an exception
+ }
+ }
+
+ // if either err indicates the user canceled the signin (expected) or a
+ // null response was sent (unexpected), invoke the .oncancel() handler.
+ if (err === 'client closed window' || !r) {
+ if (options && options.oncancel) options.oncancel();
+ delete options.oncancel;
+ }
+ });
+ };
+
+ navigator.id = {
+ request: function(options) {
+ if (this != navigator.id)
+ throw new Error("all navigator.id calls must be made on the navigator.id object");
+ options = options || {};
+ checkCompat(false);
+ // returnTo is used for post-email-verification redirect
+ if (!options.returnTo) options.returnTo = document.location.pathname;
+ return internalRequest(options);
+ },
+ watch: function(options) {
+ if (this != navigator.id)
+ throw new Error("all navigator.id calls must be made on the navigator.id object");
+ checkCompat(false);
+ internalWatch(options);
+ },
+ // logout from the current website
+ // The callback parameter is DEPRECATED, instead you should use the
+ // the .onlogout observer of the .watch() api.
+ logout: function(callback) {
+ if (this != navigator.id)
+ throw new Error("all navigator.id calls must be made on the navigator.id object");
+ // allocate iframe if it is not allocated
+ _open_hidden_iframe();
+ // send logout message if the commChan exists
+ if (commChan) commChan.notify({ method: 'logout' });
+ if (typeof callback === 'function') setTimeout(callback, 0);
+ },
+ // get an assertion
+ get: function(callback, passedOptions) {
+ var opts = {};
+ passedOptions = passedOptions || {};
+ opts.privacyPolicy = passedOptions.privacyPolicy || undefined;
+ opts.termsOfService = passedOptions.termsOfService || undefined;
+ opts.privacyURL = passedOptions.privacyURL || undefined;
+ opts.tosURL = passedOptions.tosURL || undefined;
+
+ if (checkDeprecated(passedOptions, "silent")) {
+ // Silent has been deprecated, do nothing. Placing the check here
+ // prevents the callback from being called twice, once with null and
+ // once after internalWatch has been called. See issue #1532
+ if (callback) setTimeout(function() { callback(null); }, 0);
+ return;
+ }
+
+ checkCompat(true);
+ internalWatch({
+ onlogin: function(assertion) {
+ if (callback) {
+ callback(assertion);
+ callback = null;
+ }
+ },
+ onlogout: function() {}
+ });
+ opts.oncancel = function() {
+ if (callback) {
+ callback(null);
+ callback = null;
+ }
+ observers.login = observers.logout = observers.ready = null;
+ };
+ internalRequest(opts);
+ },
+ // backwards compatibility with old API
+ getVerifiedEmail: function(callback) {
+ warn("navigator.id.getVerifiedEmail has been deprecated");
+ checkCompat(true);
+ navigator.id.get(callback);
+ },
+ // required for forwards compatibility with native implementations
+ _shimmed: true
+ };
+ }
+}());
diff --git a/templates/user.html b/templates/user.html
index 296db8c..995819c 100644
--- a/templates/user.html
+++ b/templates/user.html
@@ -1,4 +1,4 @@
-{{ template "HEADER" }}
+{{ template "HEADER" . }}
<h1>{{ .UserName }} is a bommom user.</h1>
<table class="table">
{{ range .BomList }}
@@ -8,4 +8,4 @@
No Boms found!
{{ end }}
</table>
-{{ template "FOOTER" }}
+{{ template "FOOTER" . }}