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">&nbsp;Silent <br/>
-    <input type="checkbox" id="allowPersistent">&nbsp;Allow persistent sign-in <br/>
-    <input type="text" id="requiredEmail" width="80">&nbsp;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