Bug 1060610 - Don't update latest callUrl expiration until it is exfiltrated r=Standard8
authorAdam Roach [:abr] <adam@nostrum.com>
Mon, 01 Sep 2014 17:17:05 -0500
changeset 202933 5bd6ed513f8819ff4fc443b5b6c0a07e518cfaac
parent 202932 49cb37b48c053a911487accee55e1039de48684b
child 202934 93b6e71b019a2d9ffd8c47e70319de67be1aef4f
push id27413
push userphilringnalda@gmail.com
push dateTue, 02 Sep 2014 05:46:30 +0000
treeherdermozilla-central@c360f3d1c00d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1060610
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1060610 - Don't update latest callUrl expiration until it is exfiltrated r=Standard8
browser/components/loop/content/js/client.js
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/test/desktop-local/client_test.js
browser/components/loop/test/desktop-local/panel_test.js
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -96,17 +96,17 @@ loop.Client = (function($) {
     },
 
     /**
      * Internal handler for requesting a call url from the server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
-     * -- call_url: The url of the call
+     * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     _requestCallUrlInternal: function(nickname, cb) {
       this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
@@ -115,18 +115,16 @@ loop.Client = (function($) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           var urlData = JSON.parse(responseText);
 
           cb(null, this._validate(urlData, expectedCallUrlProperties));
-
-          this.mozLoop.noteCallUrlExpiry(urlData.expiresAt);
         } catch (err) {
           console.log("Error requesting call info", err);
           cb(err);
         }
       }.bind(this));
     },
 
     /**
@@ -154,33 +152,31 @@ loop.Client = (function($) {
                                function (error, responseText) {
         if (error) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           cb(null);
-
-          this.mozLoop.noteCallUrlExpiry((new Date()).getTime() / 1000);
         } catch (err) {
           console.log("Error deleting call info", err);
           cb(err);
         }
       }.bind(this));
     },
 
     /**
      * Requests a call URL from the Loop server. It will note the
      * expiry time for the url with the mozLoop api.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
-     * -- call_url: The url of the call
+     * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     requestCallUrl: function(nickname, cb) {
       this._ensureRegistered(function(err) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -254,26 +254,28 @@ loop.panel = (function(_, mozL10n) {
           )
         )
       );
     }
   });
 
   var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
     propTypes: {
-      callUrl:  React.PropTypes.string,
-      notifier: React.PropTypes.object.isRequired,
-      client:   React.PropTypes.object.isRequired
+      callUrl:        React.PropTypes.string,
+      callUrlExpiry:  React.PropTypes.number,
+      notifier:       React.PropTypes.object.isRequired,
+      client:         React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         pending: false,
         copied: false,
-        callUrl: this.props.callUrl || ""
+        callUrl: this.props.callUrl || "",
+        callUrlExpiry: 0
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     */
@@ -302,60 +304,72 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
-          this.setState({pending: false, copied: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false,
+                         callUrl: callUrl.href,
+                         callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
         "mailto:?subject=" + __("share_email_subject3") + "&",
         "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
     handleCopyButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    handleLinkExfiltration: function(event) {
+      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      if (this.state.callUrlExpiry) {
+        navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
+      }
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       var inputCSSClass = cx({
         "pending": this.state.pending,
         // Used in functional testing, signals that
         // call url was received from loop server
          "callUrl": !this.state.pending
       });
       return (
         PanelLayout({summary: __("share_link_header_text")}, 
           React.DOM.div({className: "invite"}, 
             React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
+                   onCopy: this.handleLinkExfiltration, 
                    className: inputCSSClass}), 
             React.DOM.p({className: "btn-group url-actions"}, 
               React.DOM.button({className: "btn btn-email", disabled: !this.state.callUrl, 
                 onClick: this.handleEmailButtonClick, 
                 'data-mailto': this._generateMailTo()}, 
                 __("share_button")
               ), 
               React.DOM.button({className: "btn btn-copy", disabled: !this.state.callUrl, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -254,26 +254,28 @@ loop.panel = (function(_, mozL10n) {
           </div>
         </div>
       );
     }
   });
 
   var CallUrlResult = React.createClass({
     propTypes: {
-      callUrl:  React.PropTypes.string,
-      notifier: React.PropTypes.object.isRequired,
-      client:   React.PropTypes.object.isRequired
+      callUrl:        React.PropTypes.string,
+      callUrlExpiry:  React.PropTypes.number,
+      notifier:       React.PropTypes.object.isRequired,
+      client:         React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         pending: false,
         copied: false,
-        callUrl: this.props.callUrl || ""
+        callUrl: this.props.callUrl || "",
+        callUrlExpiry: 0
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     */
@@ -302,60 +304,72 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
-          this.setState({pending: false, copied: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false,
+                         callUrl: callUrl.href,
+                         callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
         "mailto:?subject=" + __("share_email_subject3") + "&",
         "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
     handleCopyButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    handleLinkExfiltration: function(event) {
+      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      if (this.state.callUrlExpiry) {
+        navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
+      }
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       var inputCSSClass = cx({
         "pending": this.state.pending,
         // Used in functional testing, signals that
         // call url was received from loop server
          "callUrl": !this.state.pending
       });
       return (
         <PanelLayout summary={__("share_link_header_text")}>
           <div className="invite">
             <input type="url" value={this.state.callUrl} readOnly="true"
+                   onCopy={this.handleLinkExfiltration}
                    className={inputCSSClass} />
             <p className="btn-group url-actions">
               <button className="btn btn-email" disabled={!this.state.callUrl}
                 onClick={this.handleEmailButtonClick}
                 data-mailto={this._generateMailTo()}>
                 {__("share_button")}
               </button>
               <button className="btn btn-copy" disabled={!this.state.callUrl}
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -79,30 +79,16 @@ describe("loop.Client", function() {
            // and the url.
            hawkRequestStub.callsArgWith(3, null);
 
            client.deleteCallUrl(fakeToken, callback);
 
            sinon.assert.calledWithExactly(callback, null);
          });
 
-      it("should reset all url expiry when the request succeeds", function() {
-        // Sets up the hawkRequest stub to trigger the callback with no error
-        // and the url.
-        var dateInMilliseconds = new Date(2014,7,20).getTime();
-        hawkRequestStub.callsArgWith(3, null);
-        sandbox.useFakeTimers(dateInMilliseconds);
-
-        client.deleteCallUrl(fakeToken, callback);
-
-        sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
-        sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
-                                       dateInMilliseconds / 1000);
-      });
-
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
         hawkRequestStub.callsArgWith(3, fakeErrorRes);
 
         client.deleteCallUrl(fakeToken, callback);
 
         sinon.assert.calledOnce(callback);
@@ -148,33 +134,31 @@ describe("loop.Client", function() {
           hawkRequestStub.callsArgWith(3, null,
             JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
           sinon.assert.calledWithExactly(callback, null, callUrlData);
         });
 
-      it("should note the call url expiry when the request succeeds",
+      it("should not update call url expiry when the request succeeds",
         function() {
           var callUrlData = {
             "callUrl": "fakeCallUrl",
             "expiresAt": 6000
           };
 
           // Sets up the hawkRequest stub to trigger the callback with no error
           // and the url.
           hawkRequestStub.callsArgWith(3, null,
             JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
-          sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
-          sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
-            6000);
+          sinon.assert.notCalled(mozLoop.noteCallUrlExpiry);
         });
 
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
         hawkRequestStub.callsArgWith(3, fakeErrorRes);
 
         client.requestCallUrl("foo", callback);
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -44,17 +44,18 @@ describe("loop.panel", function() {
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub().returns("unseen"),
-      copyString: sandbox.stub()
+      copyString: sandbox.stub(),
+      noteCallUrlExpiry: sinon.spy()
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     sandbox.restore();
@@ -407,26 +408,89 @@ describe("loop.panel", function() {
         fakeClient.requestCallUrl = sandbox.stub();
         var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifier: notifier,
           client: fakeClient
         }));
         view.setState({
           pending: false,
           copied: false,
-          callUrl: "http://example.com"
+          callUrl: "http://example.com",
+          callUrlExpiry: 6000
         });
 
         TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-copy"));
 
         sinon.assert.calledOnce(navigator.mozLoop.copyString);
         sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
           view.state.callUrl);
       });
 
+      it("should note the call url expiry when the url is copied via button",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-copy"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
+      it("should note the call url expiry when the url is emailed",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          view.getDOMNode().querySelector(".btn-email").dataset.mailto = "#";
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-email"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
+      it("should note the call url expiry when the url is copied manually",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          var urlField = view.getDOMNode().querySelector("input[type='url']");
+          TestUtils.Simulate.copy(urlField);
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
       it("should notify the user when the operation failed", function() {
         fakeClient.requestCallUrl = function(_, cb) {
           cb("fake error");
         };
         var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifier: notifier,
           client: fakeClient
         }));