From ef1c3cd43ce4620fc5d24beba43baf9cd1adbadc Mon Sep 17 00:00:00 2001 From: Lloyd Hilaiel <lloyd@hilaiel.com> Date: Tue, 28 Feb 2012 17:24:29 -0700 Subject: [PATCH] allow RP to specify tos and PP for display to user inside the browserid dialog. issue #841 --- example/rp/TOS.html | 5 + example/rp/index.html | 30 ++- example/rp/privacy.html | 5 + lib/static_resources.js | 2 + .../static/dialog/controllers/authenticate.js | 6 +- resources/static/dialog/controllers/dialog.js | 23 +++ .../static/dialog/controllers/pick_email.js | 6 +- .../dialog/controllers/required_email.js | 16 +- resources/static/dialog/css/popup.css | 20 +- resources/static/dialog/resources/state.js | 19 +- .../static/dialog/views/authenticate.ejs | 10 + resources/static/dialog/views/pick_email.ejs | 9 + .../static/dialog/views/required_email.ejs | 10 + resources/static/lib/urlparse.js | 191 ++++++++++++++++++ scripts/compress-locales.sh | 2 +- 15 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 example/rp/TOS.html create mode 100644 example/rp/privacy.html create mode 100644 resources/static/lib/urlparse.js diff --git a/example/rp/TOS.html b/example/rp/TOS.html new file mode 100644 index 000000000..c61b94818 --- /dev/null +++ b/example/rp/TOS.html @@ -0,0 +1,5 @@ +<html> +<body> +This is my ToS... I pour out. +</body> +</html> diff --git a/example/rp/index.html b/example/rp/index.html index 7c9492c92..d14fa1ad6 100644 --- a/example/rp/index.html +++ b/example/rp/index.html @@ -35,6 +35,9 @@ pre { word-wrap: break-word; } +.specify ul { padding-left: 0px; } +.specify li { list-style: none; } + @media screen and (max-width: 640px) { .intro, .output, .step { width: 90%; @@ -54,13 +57,26 @@ pre { </div> <div class="specify"> - What flavor of assertion would you like? <br/> - <p> - <input type="checkbox" id="silent"> Silent <br/> - <input type="checkbox" id="allowPersistent"> Allow persistent sign-in <br/> - <input type="text" id="requiredEmail" width="80"> Require a specific email <br/> + <p>What flavor of assertion would you like?</p> + <ul> + <li> + <input type="checkbox" id="silent"> + <label for="silent">Silent</label> + </li><li> + <input type="checkbox" id="allowPersistent"> + <label for="allowPersistent">Allow persistent sign-in</label> + </li><li> + <input type="checkbox" id="privacy"> + <label for="privacy">Supply a privacy policy</label> + </li><li> + <input type="checkbox" id="tos"> + <label for="tos">Supply a ToS</label> + </li><li> + <input type="text" id="requiredEmail" width="80"> + <label for="requiredEmail">Require a specific email</label><br /> + </li> + </ul> <button>Get an assertion</button> - </p> </div> <div class="verifierResp"> @@ -113,6 +129,8 @@ $(document).ready(function() { }, { silent: $('#silent').attr('checked'), allowPersistent: $('#allowPersistent').attr('checked'), + privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined, + tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined, requiredEmail: requiredEmail }); }); diff --git a/example/rp/privacy.html b/example/rp/privacy.html new file mode 100644 index 000000000..7fe9a3994 --- /dev/null +++ b/example/rp/privacy.html @@ -0,0 +1,5 @@ +<html> +<body> +This is my privacy policy. When you tip me over... +</body> +</html> diff --git a/lib/static_resources.js b/lib/static_resources.js index 5ef651474..859d3d88c 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -72,6 +72,8 @@ var dialog_min_js = '/production/:locale/dialog.js'; var dialog_js = und.flatten([ common_js, [ + '/lib/urlparse.js', + '/shared/command.js', '/shared/history.js', '/shared/state_machine.js', diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index 69ffcfab9..be2b79325 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -61,6 +61,7 @@ BrowserID.Modules.Authenticate = (function() { } else { createSecondaryUserState.call(self); } + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } } @@ -150,7 +151,9 @@ BrowserID.Modules.Authenticate = (function() { var self=this; self.renderDialog("authenticate", { sitename: user.getHostname(), - email: lastEmail + email: lastEmail, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); $(".newuser,.forgot,.returning,.start").hide(); @@ -160,6 +163,7 @@ BrowserID.Modules.Authenticate = (function() { Module.sc.start.call(self, options); initialState.call(self, options); + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } // BEGIN TESTING API diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index 74a30d092..9d5c2693b 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -81,6 +81,14 @@ BrowserID.Modules.Dialog = (function() { this.publish("window_unload"); } + function fixupURL(origin, url) { + var u; + if (/^http/.test(url)) u = URLParse(url); + else if (/^\//.test(url)) u = URLParse(origin + url); + else throw "relative urls not allowed: (" + url + ")"; + return u.validate().normalize().toString(); + } + var Dialog = bid.Modules.PageModule.extend({ start: function(options) { var self=this; @@ -115,6 +123,21 @@ BrowserID.Modules.Dialog = (function() { params = params || {}; params.hostname = user.getHostname(); + // verify params + if (params.tosURL && params.privacyURL) { + try { + params.tosURL = fixupURL(origin_url, params.tosURL); + params.privacyURL = fixupURL(origin_url, params.privacyURL); + } catch(e) { + return self.renderError("error", { + action: { + title: "error in " + origin_url, + message: "improper usage of API: " + e + } + }); + } + } + // XXX Perhaps put this into the state machine. self.bind(win, "unload", onWindowUnload); diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js index 4cdd0a1d3..f268c105f 100644 --- a/resources/static/dialog/controllers/pick_email.js +++ b/resources/static/dialog/controllers/pick_email.js @@ -97,10 +97,12 @@ BrowserID.Modules.PickEmail = (function() { identities: getSortedIdentities(), siteemail: storage.site.get(origin, "email"), allow_persistent: options.allow_persistent || false, - remember: storage.site.get(origin, "remember") || false + remember: storage.site.get(origin, "remember") || false, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); dom.getElements("body").css("opacity", "1"); - + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); if (dom.getElements("#selectEmail input[type=radio]:visible").length === 0) { // If there is only one email address, the radio button is never shown, // instead focus the sign in button so that the user can click enter. diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js index c5fb3806e..788a1f2dc 100644 --- a/resources/static/dialog/controllers/required_email.js +++ b/resources/static/dialog/controllers/required_email.js @@ -116,13 +116,16 @@ BrowserID.Modules.RequiredEmail = (function() { // a user could not be looking at stale data and/or authenticate as // somebody else. var emailInfo = user.getStoredEmailKeypair(email); + //alert(auth_level + ' ' + JSON.stringify(emailInfo) + JSON.stringify(options)); if(emailInfo && emailInfo.type === "secondary") { // secondary user, show the password field if they are not // authenticated to the "password" level. showTemplate({ signin: true, password: auth_level !== "password", - secondary_auth: secondaryAuth + secondary_auth: secondaryAuth, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); ready(); } @@ -159,7 +162,9 @@ BrowserID.Modules.RequiredEmail = (function() { // user is authenticated, but does not control address // OR // address is unknown, make the user verify. - showTemplate({ verify: true }); + showTemplate({ verify: true, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); } else { // We've made it all this way. It is a user who is not logged in @@ -178,14 +183,19 @@ BrowserID.Modules.RequiredEmail = (function() { signin: false, password: false, secondary_auth: false, - primary: false + primary: false, + privacy_url: undefined, + tos_url: undefined }, options); + self.renderDialog("required_email", options); self.click("#sign_in", signIn); self.click("#verify_address", verifyAddress); self.click("#forgotPassword", forgotPassword); self.click("#cancel", cancel); + + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } RequiredEmail.sc.start.call(self, options); diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css index 0d477ae38..674514023 100644 --- a/resources/static/dialog/css/popup.css +++ b/resources/static/dialog/css/popup.css @@ -229,6 +229,10 @@ div#required_email { right: 52px; } +#signIn .submit { + margin-left: 20px; +} + #signIn .submit > p { line-height: 13px; clear: right; @@ -295,6 +299,21 @@ label.selectable { text-shadow: 1px 1px 0 rgba(255,255,255,0.5); } +#signIn .submit > p.tospp { + /* width comes from controller/<page>.js p.tospp.css('width') update */ + color: #333; + font-size: 11px; + line-height: 1.3; + position: absolute; + text-align: justify; + bottom: 0px; + left; 0px; +} + +.tospp a { + color: #549FDC; +} + footer .learn a { color: #549FDC; } @@ -403,4 +422,3 @@ a.emphasize { #checkemail { text-align: center; } - diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index 9759b9a3c..2b6a0fceb 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -37,6 +37,8 @@ BrowserID.State = (function() { self.hostname = info.hostname; self.allowPersistent = !!info.allowPersistent; + self.privacyURL = info.privacyURL; + self.tosURL = info.tosURL; requiredEmail = info.requiredEmail; if ((typeof(requiredEmail) !== "undefined") && (!bid.verifyEmail(requiredEmail))) { @@ -68,7 +70,9 @@ BrowserID.State = (function() { if (requiredEmail) { startState("doAuthenticateWithRequiredEmail", { - email: requiredEmail + email: requiredEmail, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); } else if (authenticated) { @@ -79,6 +83,9 @@ BrowserID.State = (function() { }); subscribe("authenticate", function(msg, info) { + info = info || {}; + info.privacyURL = self.privacyURL; + info.tosURL = self.tosURL; startState("doAuthenticate", info); }); @@ -127,7 +134,7 @@ BrowserID.State = (function() { else if(info.add) { // Add the pick_email in case the user cancels the add_email screen. // The user needs something to go "back" to. - publish("pick_email", info); + publish("pick_email"); publish("add_email", info); } else { @@ -153,7 +160,9 @@ BrowserID.State = (function() { subscribe("pick_email", function() { startState("doPickEmail", { origin: self.hostname, - allow_persistent: self.allowPersistent + allow_persistent: self.allowPersistent, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); }); @@ -188,7 +197,9 @@ BrowserID.State = (function() { // screen. startState("doAuthenticateWithRequiredEmail", { email: email, - secondary_auth: true + secondary_auth: true, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); } else { diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs index 38292d152..c23560dba 100644 --- a/resources/static/dialog/views/authenticate.ejs +++ b/resources/static/dialog/views/authenticate.ejs @@ -55,6 +55,16 @@ </ul> <div class="submit cf"> + <% if (privacy_url && tos_url) { %> + <p class="tospp"> + <%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('next'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + </p> + <% } %> <button class="start" tabindex="3"><%= gettext('next') %></button> <button class="newuser" tabindex="3"><%= gettext('verify email') %></button> diff --git a/resources/static/dialog/views/pick_email.ejs b/resources/static/dialog/views/pick_email.ejs index a770a168e..a7abeeb29 100644 --- a/resources/static/dialog/views/pick_email.ejs +++ b/resources/static/dialog/views/pick_email.ejs @@ -21,6 +21,15 @@ <a id="useNewEmail" class="emphasize" href="#"><%= gettext('Use a different email') %></a> <div class="submit add cf"> + <% if (privacy_url && tos_url) { %> + <p class="tospp"><%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('sign in'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + </p> + <% } %> <% if (allow_persistent) { %> <label for="remember" class="selectable"> diff --git a/resources/static/dialog/views/required_email.ejs b/resources/static/dialog/views/required_email.ejs index 2f8cf5472..f80c47e95 100644 --- a/resources/static/dialog/views/required_email.ejs +++ b/resources/static/dialog/views/required_email.ejs @@ -52,6 +52,16 @@ </ul> <div class="submit cf"> + <% if (privacy_url && tos_url) { %> + <p class="tospp"> + <%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('sign in'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + </p> + <% } %> <% if (signin) { %> <button id="sign_in" tabindex="3"><%= gettext("sign in") %></button> <% } else if (verify) { %> diff --git a/resources/static/lib/urlparse.js b/resources/static/lib/urlparse.js new file mode 100644 index 000000000..a4fe85475 --- /dev/null +++ b/resources/static/lib/urlparse.js @@ -0,0 +1,191 @@ +/** + * urlparse.js + * + * Includes parseUri (c) Steven Levithan <steven@levithan.com> Under the MIT License + * + * Features: + * + parse a url into components + * + url validiation + * + semantically lossless normalization + * + url prefix matching + * + * window.URLParse(string) - + * parse a url using the 'parseUri' algorithm, returning an object containing various + * uri components. returns an object with the following properties (all optional): + * + * PROPERTIES: + * anchor - stuff after the # + * authority - everything after the :// and before the path. Including user auth, host, and port + * directory - path with trailing filename and everything after removed + * file - path without directory + * host - host + * password - password part when user:pass@ is prepended to host + * path - full path, sans query or anchor + * port - port, when present in url + * query - ?XXX + * relative - + * scheme - url scheme (http, file, https, etc.) + * source - full string passed to URLParse() + * user - user part when user:pass@ is prepended to host + * userInfo - + * + * FUNCTIONS: + * (string) toString() - generate a string representation of the url + * + * (this) validate() - validate the url, possbly throwing a string exception + * if determined to not be a valid URL. Returns this, thus may be chained. + * + * (this) normalize() - perform in-place modification of the url to place it in a normal + * (and verbose) form. Returns this, thus may be chained. + * + * (bool) contains(str) - returns whether the object upon which contains() is called is a + * "url prefix" for the passed in string, after normalization. + * + * (this) originOnly() - removes everything that would occur after port, including + * path, query, and anchor. + * + */ + +(function() { + /* const */ var INV_URL = "invalid url: "; + var parseURL = function(s) { + var toString = function() { + var str = this.scheme + "://"; + if (this.user) str += this.user; + if (this.password) str += ":" + this.password; + if (this.user || this.password) str += "@"; + if (this.host) str += this.host; + if (this.port) str += ":" + this.port; + if (this.path) str += this.path; + if (this.query) str += "?" + this.query; + if (this.anchor) str += "#" + this.anchor; + return str; + }; + + var originOnly = function() { + this.path = this.query = this.anchor = undefined; + return this; + }; + + var validate = function() { + if (!this.scheme) throw INV_URL +"missing scheme"; + if (this.scheme !== 'http' && this.scheme !== 'https') + throw INV_URL + "unsupported scheme: " + this.scheme; + if (!this.host) throw INV_URL + "missing host"; + if (this.port) { + var p = parseInt(this.port); + if (!this.port.match(/^\d+$/)) throw INV_URL + "non-numeric numbers in port"; + if (p <= 0 || p >= 65536) throw INV_URL + "port out of range (" +this.port+")"; + } + if (this.path && this.path.indexOf('/') != 0) throw INV_URL + "path must start with '/'"; + + return this; + }; + + var normalize = function() { + // lowercase scheme + if (this.scheme) this.scheme = this.scheme.toLowerCase(); + + // for directory references, append trailing slash + if (!this.path) this.path = "/"; + + // remove port numbers same as default + if (this.port === "80" && 'http' === this.scheme) delete this.port; + if (this.port === "443" && 'https' === this.scheme) delete this.port; + + // remove dot segments from path, algorithm + // http://tools.ietf.org/html/rfc3986#section-5.2.4 + this.path = (function (p) { + var out = []; + while (p) { + if (p.indexOf('../') === 0) p = p.substr(3); + else if (p.indexOf('./') === 0) p = p.substr(2); + else if (p.indexOf('/./') === 0) p = p.substr(2); + else if (p === '/.') p = '/'; + else if (p.indexOf('/../') === 0 || p === '/..') { + if (out.length > 0) out.pop(); + p = '/' + p.substr(4); + } else if (p === '.' || p === '..') p = ''; + else { + var m = p.match(/^\/?([^\/]*)/); + // remove path match from input + p = p.substr(m[0].length); + // add path to output + out.push(m[1]); + } + } + return '/' + out.join('/'); + })(this.path); + + // XXX: upcase chars in % escaping? + + // now we need to update all members + var n = parseURL(this.toString()), + i = 14, + o = parseUri.options; + + while (i--) { + var k = o.key[i]; + if (n[k] && typeof(n[k]) === 'string') this[k] = n[k]; + else if (this[k] && typeof(this[k]) === 'string') delete this[k]; + } + + return this; + }; + + var contains = function(str) { + try { + this.validate(); + var prefix = parseURL(this.toString()).normalize().toString(); + var url = parseURL(str).validate().normalize().toString(); + return (url.indexOf(prefix) === 0); + } catch(e) { + console.log(e); + // if any exceptions are raised, then the comparison fails + return false; + } + }; + + // parseUri 1.2.2 + // (c) Steven Levithan <stevenlevithan.com> + // MIT License + var parseUri = function(str) { + var o = parseUri.options, + m = o.parser.exec(str), + uri = {}, + i = 14; + + while (i--) if (m[i]) uri[o.key[i]] = m[i]; + + if (uri[o.key[12]]) { + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) uri[o.q.name][$1] = $2; + }); + } + // member functions + uri.toString = toString; + uri.validate = validate; + uri.normalize = normalize; + uri.contains = contains; + uri.originOnly = originOnly; + return uri; + }; + + parseUri.options = { + key: ["source","scheme","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ + }; + // end parseUri + + // parse URI using the parseUri code and return the resultant object + return parseUri(s); + }; + + if (typeof exports === 'undefined') window.URLParse = parseURL; + else module.exports = parseURL; +})(); diff --git a/scripts/compress-locales.sh b/scripts/compress-locales.sh index 6264656b5..d3a7c0ec7 100755 --- a/scripts/compress-locales.sh +++ b/scripts/compress-locales.sh @@ -52,7 +52,7 @@ for locale in $locales; do mkdir -p $BUILD_PATH/../i18n/$locale # Touch as the trigger locale doesn't really exist touch $BUILD_PATH/../i18n/${locale}/client.json - cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js + cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js lib/urlparse.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js done echo '' -- GitLab