mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
882b7f5864
@ -158,7 +158,7 @@ Site.prototype = {
|
||||
thumbnail.style.backgroundColor = this.link.bgColor;
|
||||
}
|
||||
let uri = this.link.imageURI || PageThumbs.getThumbnailURL(this.url);
|
||||
thumbnail.style.backgroundImage = "url(" + uri + ")";
|
||||
thumbnail.style.backgroundImage = 'url("' + uri + '")';
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -42,19 +42,6 @@ function injectLoopAPI(targetWindow) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the url for the Loop server from preferences.
|
||||
*
|
||||
* @return {String} The Loop server url
|
||||
*/
|
||||
serverUrl: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return Services.prefs.getCharPref("loop.server");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current locale of the browser.
|
||||
*
|
||||
@ -214,7 +201,40 @@ function injectLoopAPI(targetWindow) {
|
||||
ringer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a hawk based request to the loop server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - {Object|null} null if success. Otherwise an object:
|
||||
* {
|
||||
* code: 401,
|
||||
* errno: 401,
|
||||
* error: "Request failed",
|
||||
* message: "invalid token"
|
||||
* }
|
||||
* - {String} The body of the response.
|
||||
*
|
||||
* @param {String} path The path to make the request to.
|
||||
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
||||
* @param {Object} payloadObj An object which is converted to JSON and
|
||||
* transmitted with the request.
|
||||
* @param {Function} callback Called when the request completes.
|
||||
*/
|
||||
hawkRequest: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(path, method, payloadObj, callback) {
|
||||
// XXX Should really return a DOM promise here.
|
||||
return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
|
||||
callback(null, response.body);
|
||||
}, (error) => {
|
||||
callback(Cu.cloneInto(error, targetWindow));
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let contentObj = Cu.createObjectIn(targetWindow);
|
||||
|
@ -147,6 +147,11 @@ let MozLoopServiceInternal = {
|
||||
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
||||
* @param {Object} payloadObj An object which is converted to JSON and
|
||||
* transmitted with the request.
|
||||
* @returns {Promise}
|
||||
* Returns a promise that resolves to the response of the API call,
|
||||
* or is rejected with an error. If the server response can be parsed
|
||||
* as JSON and contains an 'error' property, the promise will be
|
||||
* rejected with this JSON-parsed response.
|
||||
*/
|
||||
hawkRequest: function(path, method, payloadObj) {
|
||||
if (!this._hawkClient) {
|
||||
@ -162,7 +167,9 @@ let MozLoopServiceInternal = {
|
||||
|
||||
let credentials;
|
||||
if (sessionToken) {
|
||||
credentials = deriveHawkCredentials(sessionToken, "sessionToken", 2 * 32);
|
||||
// true = use a hex key, as required by the server (see bug 1032738).
|
||||
credentials = deriveHawkCredentials(sessionToken, "sessionToken",
|
||||
2 * 32, true);
|
||||
}
|
||||
|
||||
return this._hawkClient.request(path, method, credentials, payloadObj);
|
||||
@ -481,5 +488,22 @@ this.MozLoopService = {
|
||||
"; exception: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a hawk based request to the loop server.
|
||||
*
|
||||
* @param {String} path The path to make the request to.
|
||||
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
||||
* @param {Object} payloadObj An object which is converted to JSON and
|
||||
* transmitted with the request.
|
||||
* @returns {Promise}
|
||||
* Returns a promise that resolves to the response of the API call,
|
||||
* or is rejected with an error. If the server response can be parsed
|
||||
* as JSON and contains an 'error' property, the promise will be
|
||||
* rejected with this JSON-parsed response.
|
||||
*/
|
||||
hawkRequest: function(path, method, payloadObj) {
|
||||
return MozLoopServiceInternal.hawkRequest(path, method, payloadObj);
|
||||
},
|
||||
};
|
||||
|
@ -21,14 +21,11 @@
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||
</body>
|
||||
|
196
browser/components/loop/content/js/client.js
Normal file
196
browser/components/loop/content/js/client.js
Normal file
@ -0,0 +1,196 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.Client = (function($) {
|
||||
"use strict";
|
||||
|
||||
// The expected properties to be returned from the POST /call-url/ request.
|
||||
const expectedCallUrlProperties = ["callUrl", "expiresAt"];
|
||||
|
||||
// The expected properties to be returned from the GET /calls request.
|
||||
const expectedCallProperties = ["calls"];
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function Client(settings = {}) {
|
||||
|
||||
// allowing an |in| test rather than a more type || allows us to dependency
|
||||
// inject a non-existent mozLoop
|
||||
if ("mozLoop" in settings) {
|
||||
this.mozLoop = settings.mozLoop;
|
||||
} else {
|
||||
this.mozLoop = navigator.mozLoop;
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
Client.prototype = {
|
||||
/**
|
||||
* Converts from hours to seconds
|
||||
*/
|
||||
_hoursToSeconds: function(value) {
|
||||
return value * 60 * 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length == 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param {Object} error See MozLoopAPI.hawkRequest
|
||||
*/
|
||||
_failureHandler: function(cb, error) {
|
||||
var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the client is registered with the push server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
*/
|
||||
_ensureRegistered: function(cb) {
|
||||
this.mozLoop.ensureRegistered(cb);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* -- 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},
|
||||
(error, responseText) => {
|
||||
if (error) {
|
||||
this._failureHandler(cb, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var urlData = JSON.parse(responseText);
|
||||
|
||||
// XXX Support an alternate call_url property for
|
||||
// backwards compatibility whilst we switch over servers.
|
||||
// Bug 1033988 will want to remove these two lines.
|
||||
if (urlData.call_url)
|
||||
urlData.callUrl = urlData.call_url;
|
||||
|
||||
cb(null, this._validate(urlData, expectedCallUrlProperties));
|
||||
|
||||
var expiresHours = this._hoursToSeconds(urlData.expiresAt);
|
||||
this.mozLoop.noteCallUrlExpiry(expiresHours);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* -- 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) {
|
||||
if (err) {
|
||||
console.log("Error registering with Loop server, code: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestCallUrlInternal(nickname, cb);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests call information from the server for all calls since the
|
||||
* given version.
|
||||
*
|
||||
* @param {String} version the version identifier from the push
|
||||
* notification
|
||||
* @param {Function} cb Callback(err, calls)
|
||||
*/
|
||||
requestCallsInfo: function(version, cb) {
|
||||
// XXX It is likely that we'll want to move some of this to whatever
|
||||
// opens the chat window, but we'll need to decide on this in bug 1002418
|
||||
if (!version) {
|
||||
throw new Error("missing required parameter version");
|
||||
}
|
||||
|
||||
this.mozLoop.hawkRequest("/calls?version=" + version, "GET", null,
|
||||
(error, responseText) => {
|
||||
if (error) {
|
||||
this._failureHandler(cb, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var callsData = JSON.parse(responseText);
|
||||
|
||||
cb(null, this._validate(callsData, expectedCallProperties));
|
||||
} catch (err) {
|
||||
console.log("Error requesting calls info", err);
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return Client;
|
||||
})(jQuery);
|
@ -137,7 +137,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
accept: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
baseServerUrl: window.navigator.mozLoop.serverUrl,
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
});
|
||||
},
|
||||
|
@ -49,6 +49,24 @@ loop.panel = (function(_, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
var ToSView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p data-l10n-id="legal_text_and_links"',
|
||||
' data-l10n-args=\'',
|
||||
' {"terms_of_use_url": "https://accounts.firefox.com/legal/terms",',
|
||||
' "privacy_notice_url": "www.mozilla.org/privacy/"',
|
||||
' }\'></p>'
|
||||
].join('')),
|
||||
|
||||
render: function() {
|
||||
if (navigator.mozLoop.getLoopCharPref('seenToS') === null) {
|
||||
this.$el.html(this.template());
|
||||
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel view.
|
||||
*/
|
||||
@ -63,6 +81,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
' <button type="submit" class="get-url btn btn-success"',
|
||||
' data-l10n-id="get_a_call_url"></button>',
|
||||
' </form>',
|
||||
' <p class="tos"></p>',
|
||||
' <p class="result hide">',
|
||||
' <input id="call-url" type="url" readonly>',
|
||||
' <a class="go-back btn btn-info" href="" data-l10n-id="new_url"></a>',
|
||||
@ -91,9 +110,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this.notifier = options.notifier;
|
||||
this.client = new loop.shared.Client({
|
||||
baseServerUrl: navigator.mozLoop.serverUrl
|
||||
});
|
||||
this.client = new loop.Client();
|
||||
},
|
||||
|
||||
getNickname: function() {
|
||||
@ -129,7 +146,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
this.notifier.clear();
|
||||
this.$(".action .invite").hide();
|
||||
this.$(".action .invite input").val("");
|
||||
this.$(".action .result input").val(callUrlData.call_url);
|
||||
this.$(".action .result input").val(callUrlData.callUrl);
|
||||
this.$(".action .result").show();
|
||||
this.$(".description p").text(__("share_link_url"));
|
||||
},
|
||||
@ -158,6 +175,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
this.$el.html(this.template());
|
||||
// Do not Disturb sub view
|
||||
this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
|
||||
this.tosView = new ToSView({el: this.$(".tos")}).render();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@ -243,6 +261,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
init: init,
|
||||
PanelView: PanelView,
|
||||
DoNotDisturbView: DoNotDisturbView,
|
||||
PanelRouter: PanelRouter
|
||||
PanelRouter: PanelRouter,
|
||||
ToSView: ToSView
|
||||
};
|
||||
})(_, document.mozL10n);
|
||||
|
@ -19,14 +19,11 @@
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/panel.js"></script>
|
||||
</body>
|
||||
|
@ -4,6 +4,10 @@
|
||||
|
||||
/* Panel styles */
|
||||
|
||||
a {
|
||||
color: #0095DD;
|
||||
}
|
||||
|
||||
.panel {
|
||||
/* XXX the Social API panel behaves weirdly on inner element size changes,
|
||||
adding unwanted scrollbars; quickfix is to hide these for now. */
|
||||
@ -76,6 +80,13 @@
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.tos {
|
||||
font-size: .6rem;
|
||||
color: #a8a8a8;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Specific cases */
|
||||
|
||||
.panel #messages .alert {
|
||||
|
@ -1,362 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.Client = (function($) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function Client(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.hasOwnProperty("baseServerUrl") ||
|
||||
!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
// allowing an |in| test rather than a more type || allows us to dependency
|
||||
// inject a non-existent mozLoop
|
||||
if ("mozLoop" in settings) {
|
||||
this.mozLoop = settings.mozLoop;
|
||||
} else {
|
||||
this.mozLoop = navigator.mozLoop;
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
Client.prototype = {
|
||||
/**
|
||||
* Converts from hours to seconds
|
||||
*/
|
||||
_hoursToSeconds: function(value) {
|
||||
return value * 60 * 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length <= 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param jqXHR See jQuery docs
|
||||
* @param textStatus See jQuery docs
|
||||
* @param errorThrown See jQuery docs
|
||||
*/
|
||||
_failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
|
||||
var error = "Unknown error.",
|
||||
jsonRes = jqXHR && jqXHR.responseJSON || {};
|
||||
// Received error response format:
|
||||
// { "status": "errors",
|
||||
// "errors": [{
|
||||
// "location": "url",
|
||||
// "name": "token",
|
||||
// "description": "invalid token"
|
||||
// }]}
|
||||
if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
|
||||
error = "Details: " + jsonRes.errors.map(function(err) {
|
||||
return Object.keys(err).map(function(field) {
|
||||
return field + ": " + err[field];
|
||||
}).join(", ");
|
||||
}).join("; ");
|
||||
}
|
||||
var message = "HTTP " + jqXHR.status + " " + errorThrown +
|
||||
"; " + error;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the client is registered with the push server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
*/
|
||||
_ensureRegistered: function(cb) {
|
||||
navigator.mozLoop.ensureRegistered(function(err) {
|
||||
cb(err);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures that the client picks up the hawk-session-token
|
||||
* put in preferences by the LoopService registration code,
|
||||
* derives hawk credentials from them, and saves them in
|
||||
* this._credentials.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* if err is set to null in the callback, that indicates that the
|
||||
* credentials have been successfully attached to this object.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @note That as currently written, this is only ever expected to be called
|
||||
* from browser UI code (ie it relies on mozLoop).
|
||||
*/
|
||||
_ensureCredentials: function(cb) {
|
||||
if (this._credentials) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var hawkSessionToken =
|
||||
this.mozLoop.getLoopCharPref("hawk-session-token");
|
||||
if (!hawkSessionToken) {
|
||||
var msg = "loop.hawk-session-token pref not found";
|
||||
console.warn(msg);
|
||||
cb(new Error(msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX do we want to use any of the other hawk params (eg to track clock
|
||||
// skew, etc)?
|
||||
var serverDerivedKeyLengthInBytes = 2 * 32;
|
||||
deriveHawkCredentials(hawkSessionToken, "sessionToken",
|
||||
serverDerivedKeyLengthInBytes, function (hawkCredentials) {
|
||||
this._credentials = hawkCredentials;
|
||||
cb(null);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* -- 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) {
|
||||
var endpoint = this.settings.baseServerUrl + "/call-url/",
|
||||
reqData = {callerId: nickname};
|
||||
|
||||
var req = $.ajax({
|
||||
type: "POST",
|
||||
url: endpoint,
|
||||
data: reqData,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callUrlData) {
|
||||
// XXX split this out into two functions for better readability
|
||||
try {
|
||||
cb(null, this._validate(callUrlData, ["call_url", "expiresAt"]));
|
||||
|
||||
var expiresHours = this._hoursToSeconds(callUrlData.expiresAt);
|
||||
navigator.mozLoop.noteCallUrlExpiry(expiresHours);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* -- 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) {
|
||||
if (err) {
|
||||
console.log("Error registering with Loop server, code: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallUrlInternal(nickname, cb);
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests call information from the server for all calls since the
|
||||
* given version.
|
||||
*
|
||||
* @param {String} version the version identifier from the push
|
||||
* notification
|
||||
* @param {Function} cb Callback(err, calls)
|
||||
*/
|
||||
requestCallsInfo: function(version, cb) {
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallsInfoInternal(version, cb);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_requestCallsInfoInternal: function(version, cb) {
|
||||
if (!version) {
|
||||
throw new Error("missing required parameter version");
|
||||
}
|
||||
|
||||
var endpoint = this.settings.baseServerUrl + "/calls";
|
||||
|
||||
// XXX It is likely that we'll want to move some of this to whatever
|
||||
// opens the chat window, but we'll need to decide that once we make a
|
||||
// decision on chrome versus content, and know if we're going with
|
||||
// LoopService or a frameworker.
|
||||
var req = $.ajax({
|
||||
type: "GET",
|
||||
url: endpoint + "?version=" + version,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callsData) {
|
||||
try {
|
||||
cb(null, this._validate(callsData, ["calls"]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting calls info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts a call request to the server for a call represented by the
|
||||
* loopToken. Will return the session data for the call.
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var req = $.ajax({
|
||||
url: this.settings.baseServerUrl + "/calls/" + loopToken,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.done(function(sessionData) {
|
||||
try {
|
||||
cb(null, this._validate(sessionData, [
|
||||
"sessionId", "sessionToken", "apiKey"
|
||||
]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* If this._credentials is set, adds a hawk Authorization header based
|
||||
* based on those credentials to the passed-in XHR.
|
||||
*
|
||||
* @param xhr request to add any header to
|
||||
* @param settings settings object passed to jQuery.ajax()
|
||||
* @private
|
||||
*/
|
||||
_attachAnyServerCreds: function(xhr, settings) {
|
||||
// if the server needs credentials and didn't get them, it will
|
||||
// return failure for us, so if we don't have any creds, don't try to
|
||||
// attach them.
|
||||
if (!this._credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
var header = hawk.client.header(settings.url, settings.type,
|
||||
{ credentials: this._credentials });
|
||||
if (header.err) {
|
||||
throw new Error(header.err);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader("Authorization", header.field);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return Client;
|
||||
})(jQuery);
|
@ -62,9 +62,13 @@ loop.shared.models = (function() {
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* - {String} baseServerUrl The server URL
|
||||
* - {Boolean} outgoing Set to true if this model represents the
|
||||
* outgoing call.
|
||||
* - {Boolean} callType Only valid for outgoing calls. The type of media in
|
||||
* the call, e.g. "audio" or "audio-video"
|
||||
* - {loop.shared.Client} client A client object to request call information
|
||||
* from. Expects requestCallInfo for outgoing
|
||||
* calls, requestCallsInfo for incoming calls.
|
||||
*
|
||||
* Triggered events:
|
||||
*
|
||||
@ -75,10 +79,6 @@ loop.shared.models = (function() {
|
||||
* @param {Object} options Options object
|
||||
*/
|
||||
initiate: function(options) {
|
||||
var client = new loop.shared.Client({
|
||||
baseServerUrl: options.baseServerUrl
|
||||
});
|
||||
|
||||
function handleResult(err, sessionData) {
|
||||
/*jshint validthis:true */
|
||||
if (err) {
|
||||
@ -99,10 +99,11 @@ loop.shared.models = (function() {
|
||||
}
|
||||
|
||||
if (options.outgoing) {
|
||||
client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
||||
options.client.requestCallInfo(this.get("loopToken"), options.callType,
|
||||
handleResult.bind(this));
|
||||
}
|
||||
else {
|
||||
client.requestCallsInfo(this.get("loopVersion"),
|
||||
options.client.requestCallsInfo(this.get("loopVersion"),
|
||||
handleResult.bind(this));
|
||||
}
|
||||
},
|
||||
|
@ -1,556 +0,0 @@
|
||||
/*
|
||||
HTTP Hawk Authentication Scheme
|
||||
Copyright (c) 2012-2014, Eran Hammer <eran@hammer.io>
|
||||
BSD Licensed
|
||||
*/
|
||||
|
||||
|
||||
// Declare namespace
|
||||
|
||||
var hawk = {
|
||||
internals: {}
|
||||
};
|
||||
|
||||
|
||||
hawk.client = {
|
||||
|
||||
// Generate an Authorization header for a given request
|
||||
|
||||
/*
|
||||
uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri()
|
||||
method: HTTP verb (e.g. 'GET', 'POST')
|
||||
options: {
|
||||
|
||||
// Required
|
||||
|
||||
credentials: {
|
||||
id: 'dh37fgj492je',
|
||||
key: 'aoijedoaijsdlaksjdl',
|
||||
algorithm: 'sha256' // 'sha1', 'sha256'
|
||||
},
|
||||
|
||||
// Optional
|
||||
|
||||
ext: 'application-specific', // Application specific data sent via the ext attribute
|
||||
timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds
|
||||
nonce: '2334f34f', // A pre-generated nonce
|
||||
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
|
||||
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
|
||||
contentType: 'application/json', // Payload content-type (ignored if hash provided)
|
||||
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
|
||||
app: '24s23423f34dx', // Oz application id
|
||||
dlg: '234sz34tww3sd' // Oz delegated-by application id
|
||||
}
|
||||
*/
|
||||
|
||||
header: function (uri, method, options) {
|
||||
|
||||
var result = {
|
||||
field: '',
|
||||
artifacts: {}
|
||||
};
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
|
||||
!method || typeof method !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
result.err = 'Invalid argument type';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
result.err = 'Invalid credentials object';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
result.err = 'Unknown algorithm';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse URI
|
||||
|
||||
if (typeof uri === 'string') {
|
||||
uri = hawk.utils.parseUri(uri);
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
method: method,
|
||||
resource: uri.relative,
|
||||
host: uri.hostname,
|
||||
port: uri.port,
|
||||
hash: options.hash,
|
||||
ext: options.ext,
|
||||
app: options.app,
|
||||
dlg: options.dlg
|
||||
};
|
||||
|
||||
result.artifacts = artifacts;
|
||||
|
||||
// Calculate payload hash
|
||||
|
||||
if (!artifacts.hash &&
|
||||
(options.payload || options.payload === '')) {
|
||||
|
||||
artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
|
||||
}
|
||||
|
||||
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
|
||||
|
||||
// Construct header
|
||||
|
||||
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
|
||||
var header = 'Hawk id="' + credentials.id +
|
||||
'", ts="' + artifacts.ts +
|
||||
'", nonce="' + artifacts.nonce +
|
||||
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
|
||||
(hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
|
||||
'", mac="' + mac + '"';
|
||||
|
||||
if (artifacts.app) {
|
||||
header += ', app="' + artifacts.app +
|
||||
(artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
|
||||
}
|
||||
|
||||
result.field = header;
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
// Validate server response
|
||||
|
||||
/*
|
||||
request: object created via 'new XMLHttpRequest()' after response received
|
||||
artifacts: object received from header().artifacts
|
||||
options: {
|
||||
payload: optional payload received
|
||||
required: specifies if a Server-Authorization header is required. Defaults to 'false'
|
||||
}
|
||||
*/
|
||||
|
||||
authenticate: function (request, credentials, artifacts, options) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
var getHeader = function (name) {
|
||||
|
||||
return request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name);
|
||||
};
|
||||
|
||||
var wwwAuthenticate = getHeader('www-authenticate');
|
||||
if (wwwAuthenticate) {
|
||||
|
||||
// Parse HTTP WWW-Authenticate header
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attributes.ts) {
|
||||
var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
|
||||
if (tsm !== attributes.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
}
|
||||
|
||||
// Parse HTTP Server-Authorization header
|
||||
|
||||
var serverAuthorization = getHeader('server-authorization');
|
||||
if (!serverAuthorization &&
|
||||
!options.required) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var modArtifacts = {
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
method: artifacts.method,
|
||||
resource: artifacts.resource,
|
||||
host: artifacts.host,
|
||||
port: artifacts.port,
|
||||
hash: attributes.hash,
|
||||
ext: attributes.ext,
|
||||
app: artifacts.app,
|
||||
dlg: artifacts.dlg
|
||||
};
|
||||
|
||||
var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
|
||||
if (mac !== attributes.mac) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.payload &&
|
||||
options.payload !== '') {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!attributes.hash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type'));
|
||||
return (calculatedHash === attributes.hash);
|
||||
},
|
||||
|
||||
message: function (host, port, message, options) {
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!host || typeof host !== 'string' ||
|
||||
!port || typeof port !== 'number' ||
|
||||
message === null || message === undefined || typeof message !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
// Invalid credential object
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
host: host,
|
||||
port: port,
|
||||
hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
|
||||
};
|
||||
|
||||
// Construct authorization
|
||||
|
||||
var result = {
|
||||
id: credentials.id,
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
hash: artifacts.hash,
|
||||
mac: hawk.crypto.calculateMac('message', credentials, artifacts)
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true
|
||||
|
||||
var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
|
||||
if (tsm !== message.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updateClock !== false) {
|
||||
hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
hawk.crypto = {
|
||||
|
||||
headerVersion: '1',
|
||||
|
||||
algorithms: ['sha1', 'sha256'],
|
||||
|
||||
calculateMac: function (type, credentials, options) {
|
||||
|
||||
var normalized = hawk.crypto.generateNormalizedString(type, options);
|
||||
|
||||
var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
|
||||
return hmac.toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
generateNormalizedString: function (type, options) {
|
||||
|
||||
var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
|
||||
options.ts + '\n' +
|
||||
options.nonce + '\n' +
|
||||
(options.method || '').toUpperCase() + '\n' +
|
||||
(options.resource || '') + '\n' +
|
||||
options.host.toLowerCase() + '\n' +
|
||||
options.port + '\n' +
|
||||
(options.hash || '') + '\n';
|
||||
|
||||
if (options.ext) {
|
||||
normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
|
||||
}
|
||||
|
||||
normalized += '\n';
|
||||
|
||||
if (options.app) {
|
||||
normalized += options.app + '\n' +
|
||||
(options.dlg || '') + '\n';
|
||||
}
|
||||
|
||||
return normalized;
|
||||
},
|
||||
|
||||
calculatePayloadHash: function (payload, algorithm, contentType) {
|
||||
|
||||
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
|
||||
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
|
||||
hash.update(hawk.utils.parseContentType(contentType) + '\n');
|
||||
hash.update(payload);
|
||||
hash.update('\n');
|
||||
return hash.finalize().toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
calculateTsMac: function (ts, credentials) {
|
||||
|
||||
var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
|
||||
return hash.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// localStorage compatible interface
|
||||
|
||||
hawk.internals.LocalStorage = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
|
||||
this.getItem = function (key) {
|
||||
|
||||
return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null;
|
||||
};
|
||||
|
||||
this.setItem = function (key, value) {
|
||||
|
||||
this._cache[key] = String(value);
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.removeItem = function (key) {
|
||||
|
||||
delete this._cache[key];
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.clear = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
};
|
||||
|
||||
this.key = function (i) {
|
||||
|
||||
return Object.keys(this._cache)[i || 0];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
hawk.utils = {
|
||||
|
||||
storage: new hawk.internals.LocalStorage(),
|
||||
|
||||
setStorage: function (storage) {
|
||||
|
||||
var ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
hawk.utils.storage = storage;
|
||||
if (ntpOffset) {
|
||||
hawk.utils.setNtpOffset(ntpOffset);
|
||||
}
|
||||
},
|
||||
|
||||
setNtpOffset: function (offset) {
|
||||
|
||||
try {
|
||||
hawk.utils.storage.setItem('hawk_ntp_offset', offset);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[hawk] could not write to storage.');
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
getNtpOffset: function () {
|
||||
|
||||
var offset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(offset, 10);
|
||||
},
|
||||
|
||||
now: function (localtimeOffsetMsec) {
|
||||
|
||||
return Math.floor(((new Date()).getTime() + (localtimeOffsetMsec || 0)) / 1000) + hawk.utils.getNtpOffset();
|
||||
},
|
||||
|
||||
escapeHeaderAttribute: function (attribute) {
|
||||
|
||||
return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
|
||||
},
|
||||
|
||||
parseContentType: function (header) {
|
||||
|
||||
if (!header) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
|
||||
},
|
||||
|
||||
parseAuthorizationHeader: function (header, keys) {
|
||||
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something]
|
||||
if (!headerParts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheme = headerParts[1];
|
||||
if (scheme.toLowerCase() !== 'hawk') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributesString = headerParts[2];
|
||||
if (!attributesString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributes = {};
|
||||
var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
|
||||
|
||||
// Check valid attribute names
|
||||
|
||||
if (keys.indexOf($1) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
|
||||
|
||||
if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
|
||||
if (attributes.hasOwnProperty($1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
attributes[$1] = $2;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (verify !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
|
||||
randomString: function (size) {
|
||||
|
||||
var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
var len = randomSource.length;
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < size; ++i) {
|
||||
result[i] = randomSource[Math.floor(Math.random() * len)];
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
},
|
||||
|
||||
parseUri: function (input) {
|
||||
|
||||
// Based on: parseURI 1.2.2
|
||||
// http://blog.stevenlevithan.com/archives/parseuri
|
||||
// (c) Steven Levithan <stevenlevithan.com>
|
||||
// MIT License
|
||||
|
||||
var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
|
||||
|
||||
var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
|
||||
var uriByNumber = input.match(uriRegex);
|
||||
var uri = {};
|
||||
|
||||
for (var i = 0, il = keys.length; i < il; ++i) {
|
||||
uri[keys[i]] = uriByNumber[i] || '';
|
||||
}
|
||||
|
||||
if (uri.port === '') {
|
||||
uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// $lab:coverage:off$
|
||||
|
||||
// Based on: Crypto-JS v3.1.2
|
||||
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
|
||||
// http://code.google.com/p/crypto-js/
|
||||
// http://code.google.com/p/crypto-js/wiki/License
|
||||
|
||||
var CryptoJS = CryptoJS || function (h, r) { var k = {}, l = k.lib = {}, n = function () { }, f = l.Base = { extend: function (a) { n.prototype = this; var b = new n; a && b.mixIn(a); b.hasOwnProperty("init") || (b.init = function () { b.$super.init.apply(this, arguments) }); b.init.prototype = b; b.$super = this; return b }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var b in a) a.hasOwnProperty(b) && (this[b] = a[b]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, j = l.WordArray = f.extend({ init: function (a, b) { a = this.words = a || []; this.sigBytes = b != r ? b : 4 * a.length }, toString: function (a) { return (a || s).stringify(this) }, concat: function (a) { var b = this.words, d = a.words, c = this.sigBytes; a = a.sigBytes; this.clamp(); if (c % 4) for (var e = 0; e < a; e++) b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); else if (65535 < d.length) for (e = 0; e < a; e += 4) b[c + e >>> 2] = d[e >>> 2]; else b.push.apply(b, d); this.sigBytes += a; return this }, clamp: function () { var a = this.words, b = this.sigBytes; a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4); a.length = h.ceil(b / 4) }, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0); return new j.init(b, a) } }), m = k.enc = {}, s = m.Hex = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) { var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); return new j.init(d, b / 2) } }, p = m.Latin1 = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); return new j.init(d, b) } }, t = m.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(p.stringify(a))) } catch (b) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return p.parse(unescape(encodeURIComponent(a))) } }, q = l.BufferedBlockAlgorithm = f.extend({ reset: function () { this._data = new j.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = t.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var b = this._data, d = b.words, c = b.sigBytes, e = this.blockSize, f = c / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; c = h.min(4 * a, c); if (a) { for (var g = 0; g < a; g += e) this._doProcessBlock(d, g); g = d.splice(0, a); b.sigBytes -= c } return new j.init(g, c) }, clone: function () { var a = f.clone.call(this); a._data = this._data.clone(); return a }, _minBufferSize: 0 }); l.Hasher = q.extend({ cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { q.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (b, d) { return (new a.init(d)).finalize(b) } }, _createHmacHelper: function (a) { return function (b, d) { return (new u.HMAC.init(a, d)).finalize(b) } } }); var u = k.algo = {}; return k }(Math);
|
||||
(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
|
||||
(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
|
||||
(function () { var c = CryptoJS, k = c.enc.Utf8; c.algo.HMAC = c.lib.Base.extend({ init: function (a, b) { a = this._hasher = new a.init; "string" == typeof b && (b = k.parse(b)); var c = a.blockSize, e = 4 * c; b.sigBytes > e && (b = a.finalize(b)); b.clamp(); for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) h[d] ^= 1549556828, j[d] ^= 909522486; f.sigBytes = g.sigBytes = e; this.reset() }, reset: function () { var a = this._hasher; a.reset(); a.update(this._iKey) }, update: function (a) { this._hasher.update(a); return this }, finalize: function (a) { var b = this._hasher; a = b.finalize(a); b.reset(); return b.finalize(this._oKey.clone().concat(a)) } }) })();
|
||||
(function () { var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { stringify: function (b) { var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) b.push(c.charAt(d >>> 6 * (3 - g) & 63)); if (e = c.charAt(64)) for (; b.length % 4;) b.push(e); return b.join("") }, parse: function (b) { var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < e; d++) if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } return j.create(c, a) }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" } })();
|
||||
|
||||
hawk.crypto.internals = CryptoJS;
|
||||
|
||||
|
||||
// Export if used as a module
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = hawk;
|
||||
}
|
||||
|
||||
// $lab:coverage:on$
|
@ -1,606 +0,0 @@
|
||||
/** @fileOverview Javascript cryptography implementation.
|
||||
*
|
||||
* Crush to remove comments, shorten variable names and
|
||||
* generally reduce transmission size.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
|
||||
/*global document, window, escape, unescape, module, require, Uint32Array */
|
||||
|
||||
/** @namespace The Stanford Javascript Crypto Library, top-level namespace. */
|
||||
var sjcl = {
|
||||
/** @namespace Symmetric ciphers. */
|
||||
cipher: {},
|
||||
|
||||
/** @namespace Hash functions. Right now only SHA256 is implemented. */
|
||||
hash: {},
|
||||
|
||||
/** @namespace Key exchange functions. Right now only SRP is implemented. */
|
||||
keyexchange: {},
|
||||
|
||||
/** @namespace Block cipher modes of operation. */
|
||||
mode: {},
|
||||
|
||||
/** @namespace Miscellaneous. HMAC and PBKDF2. */
|
||||
misc: {},
|
||||
|
||||
/**
|
||||
* @namespace Bit array encoders and decoders.
|
||||
*
|
||||
* @description
|
||||
* The members of this namespace are functions which translate between
|
||||
* SJCL's bitArrays and other objects (usually strings). Because it
|
||||
* isn't always clear which direction is encoding and which is decoding,
|
||||
* the method names are "fromBits" and "toBits".
|
||||
*/
|
||||
codec: {},
|
||||
|
||||
/** @namespace Exceptions. */
|
||||
exception: {
|
||||
/** @constructor Ciphertext is corrupt. */
|
||||
corrupt: function(message) {
|
||||
this.toString = function() { return "CORRUPT: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Invalid parameter. */
|
||||
invalid: function(message) {
|
||||
this.toString = function() { return "INVALID: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Bug or missing feature in SJCL. @constructor */
|
||||
bug: function(message) {
|
||||
this.toString = function() { return "BUG: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Something isn't ready. */
|
||||
notReady: function(message) {
|
||||
this.toString = function() { return "NOT READY: "+this.message; };
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(typeof module !== 'undefined' && module.exports){
|
||||
module.exports = sjcl;
|
||||
}
|
||||
/** @fileOverview Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @description
|
||||
* <p>
|
||||
* These objects are the currency accepted by SJCL's crypto functions.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Most of our crypto primitives operate on arrays of 4-byte words internally,
|
||||
* but many of them can take arguments that are not a multiple of 4 bytes.
|
||||
* This library encodes arrays of bits (whose size need not be a multiple of 8
|
||||
* bits) as arrays of 32-bit words. The bits are packed, big-endian, into an
|
||||
* array of words, 32 bits at a time. Since the words are double-precision
|
||||
* floating point numbers, they fit some extra data. We use this (in a private,
|
||||
* possibly-changing manner) to encode the number of bits actually present
|
||||
* in the last word of the array.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Because bitwise ops clear this out-of-band data, these arrays can be passed
|
||||
* to ciphers like AES which want arrays of words.
|
||||
* </p>
|
||||
*/
|
||||
sjcl.bitArray = {
|
||||
/**
|
||||
* Array slices in units of bits.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} bend The offset to the end of the slice, in bits. If this is undefined,
|
||||
* slice until the end of the array.
|
||||
* @return {bitArray} The requested slice.
|
||||
*/
|
||||
bitSlice: function (a, bstart, bend) {
|
||||
a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1);
|
||||
return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract a number packed into a bit array.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} length The length of the number to extract.
|
||||
* @return {Number} The requested slice.
|
||||
*/
|
||||
extract: function(a, bstart, blength) {
|
||||
// FIXME: this Math.floor is not necessary at all, but for some reason
|
||||
// seems to suppress a bug in the Chromium JIT.
|
||||
var x, sh = Math.floor((-bstart-blength) & 31);
|
||||
if ((bstart + blength - 1 ^ bstart) & -32) {
|
||||
// it crosses a boundary
|
||||
x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh);
|
||||
} else {
|
||||
// within a single word
|
||||
x = a[bstart/32|0] >>> sh;
|
||||
}
|
||||
return x & ((1<<blength) - 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Concatenate two bit arrays.
|
||||
* @param {bitArray} a1 The first array.
|
||||
* @param {bitArray} a2 The second array.
|
||||
* @return {bitArray} The concatenation of a1 and a2.
|
||||
*/
|
||||
concat: function (a1, a2) {
|
||||
if (a1.length === 0 || a2.length === 0) {
|
||||
return a1.concat(a2);
|
||||
}
|
||||
|
||||
var last = a1[a1.length-1], shift = sjcl.bitArray.getPartial(last);
|
||||
if (shift === 32) {
|
||||
return a1.concat(a2);
|
||||
} else {
|
||||
return sjcl.bitArray._shiftRight(a2, shift, last|0, a1.slice(0,a1.length-1));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the length of an array of bits.
|
||||
* @param {bitArray} a The array.
|
||||
* @return {Number} The length of a, in bits.
|
||||
*/
|
||||
bitLength: function (a) {
|
||||
var l = a.length, x;
|
||||
if (l === 0) { return 0; }
|
||||
x = a[l - 1];
|
||||
return (l-1) * 32 + sjcl.bitArray.getPartial(x);
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate an array.
|
||||
* @param {bitArray} a The array.
|
||||
* @param {Number} len The length to truncate to, in bits.
|
||||
* @return {bitArray} A new array, truncated to len bits.
|
||||
*/
|
||||
clamp: function (a, len) {
|
||||
if (a.length * 32 < len) { return a; }
|
||||
a = a.slice(0, Math.ceil(len / 32));
|
||||
var l = a.length;
|
||||
len = len & 31;
|
||||
if (l > 0 && len) {
|
||||
a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1);
|
||||
}
|
||||
return a;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a partial word for a bit array.
|
||||
* @param {Number} len The number of bits in the word.
|
||||
* @param {Number} x The bits.
|
||||
* @param {Number} [0] _end Pass 1 if x has already been shifted to the high side.
|
||||
* @return {Number} The partial word.
|
||||
*/
|
||||
partial: function (len, x, _end) {
|
||||
if (len === 32) { return x; }
|
||||
return (_end ? x|0 : x << (32-len)) + len * 0x10000000000;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of bits used by a partial word.
|
||||
* @param {Number} x The partial word.
|
||||
* @return {Number} The number of bits used by the partial word.
|
||||
*/
|
||||
getPartial: function (x) {
|
||||
return Math.round(x/0x10000000000) || 32;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare two arrays for equality in a predictable amount of time.
|
||||
* @param {bitArray} a The first array.
|
||||
* @param {bitArray} b The second array.
|
||||
* @return {boolean} true if a == b; false otherwise.
|
||||
*/
|
||||
equal: function (a, b) {
|
||||
if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) {
|
||||
return false;
|
||||
}
|
||||
var x = 0, i;
|
||||
for (i=0; i<a.length; i++) {
|
||||
x |= a[i]^b[i];
|
||||
}
|
||||
return (x === 0);
|
||||
},
|
||||
|
||||
/** Shift an array right.
|
||||
* @param {bitArray} a The array to shift.
|
||||
* @param {Number} shift The number of bits to shift.
|
||||
* @param {Number} [carry=0] A byte to carry in
|
||||
* @param {bitArray} [out=[]] An array to prepend to the output.
|
||||
* @private
|
||||
*/
|
||||
_shiftRight: function (a, shift, carry, out) {
|
||||
var i, last2=0, shift2;
|
||||
if (out === undefined) { out = []; }
|
||||
|
||||
for (; shift >= 32; shift -= 32) {
|
||||
out.push(carry);
|
||||
carry = 0;
|
||||
}
|
||||
if (shift === 0) {
|
||||
return out.concat(a);
|
||||
}
|
||||
|
||||
for (i=0; i<a.length; i++) {
|
||||
out.push(carry | a[i]>>>shift);
|
||||
carry = a[i] << (32-shift);
|
||||
}
|
||||
last2 = a.length ? a[a.length-1] : 0;
|
||||
shift2 = sjcl.bitArray.getPartial(last2);
|
||||
out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1));
|
||||
return out;
|
||||
},
|
||||
|
||||
/** xor a block of 4 words together.
|
||||
* @private
|
||||
*/
|
||||
_xor4: function(x,y) {
|
||||
return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]];
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace UTF-8 strings */
|
||||
sjcl.codec.utf8String = {
|
||||
/** Convert from a bitArray to a UTF-8 string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||||
for (i=0; i<bl/8; i++) {
|
||||
if ((i&3) === 0) {
|
||||
tmp = arr[i/4];
|
||||
}
|
||||
out += String.fromCharCode(tmp >>> 24);
|
||||
tmp <<= 8;
|
||||
}
|
||||
return decodeURIComponent(escape(out));
|
||||
},
|
||||
|
||||
/** Convert from a UTF-8 string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
str = unescape(encodeURIComponent(str));
|
||||
var out = [], i, tmp=0;
|
||||
for (i=0; i<str.length; i++) {
|
||||
tmp = tmp << 8 | str.charCodeAt(i);
|
||||
if ((i&3) === 3) {
|
||||
out.push(tmp);
|
||||
tmp = 0;
|
||||
}
|
||||
}
|
||||
if (i&3) {
|
||||
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Hexadecimal */
|
||||
sjcl.codec.hex = {
|
||||
/** Convert from a bitArray to a hex string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", i;
|
||||
for (i=0; i<arr.length; i++) {
|
||||
out += ((arr[i]|0)+0xF00000000000).toString(16).substr(4);
|
||||
}
|
||||
return out.substr(0, sjcl.bitArray.bitLength(arr)/4);//.replace(/(.{8})/g, "$1 ");
|
||||
},
|
||||
/** Convert from a hex string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
var i, out=[], len;
|
||||
str = str.replace(/\s|0x/g, "");
|
||||
len = str.length;
|
||||
str = str + "00000000";
|
||||
for (i=0; i<str.length; i+=8) {
|
||||
out.push(parseInt(str.substr(i,8),16)^0);
|
||||
}
|
||||
return sjcl.bitArray.clamp(out, len*4);
|
||||
}
|
||||
};
|
||||
|
||||
/** @fileOverview Javascript SHA-256 implementation.
|
||||
*
|
||||
* An older version of this implementation is available in the public
|
||||
* domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh,
|
||||
* Stanford University 2008-2010 and BSD-licensed for liability
|
||||
* reasons.
|
||||
*
|
||||
* Special thanks to Aldo Cortesi for pointing out several bugs in
|
||||
* this code.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/**
|
||||
* Context for a SHA-256 operation in progress.
|
||||
* @constructor
|
||||
* @class Secure Hash Algorithm, 256 bits.
|
||||
*/
|
||||
sjcl.hash.sha256 = function (hash) {
|
||||
if (!this._key[0]) { this._precompute(); }
|
||||
if (hash) {
|
||||
this._h = hash._h.slice(0);
|
||||
this._buffer = hash._buffer.slice(0);
|
||||
this._length = hash._length;
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash a string or an array of words.
|
||||
* @static
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return {bitArray} The hash value, an array of 16 big-endian words.
|
||||
*/
|
||||
sjcl.hash.sha256.hash = function (data) {
|
||||
return (new sjcl.hash.sha256()).update(data).finalize();
|
||||
};
|
||||
|
||||
sjcl.hash.sha256.prototype = {
|
||||
/**
|
||||
* The hash's block size, in bits.
|
||||
* @constant
|
||||
*/
|
||||
blockSize: 512,
|
||||
|
||||
/**
|
||||
* Reset the hash state.
|
||||
* @return this
|
||||
*/
|
||||
reset:function () {
|
||||
this._h = this._init.slice(0);
|
||||
this._buffer = [];
|
||||
this._length = 0;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Input several words to the hash.
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return this
|
||||
*/
|
||||
update: function (data) {
|
||||
if (typeof data === "string") {
|
||||
data = sjcl.codec.utf8String.toBits(data);
|
||||
}
|
||||
var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data),
|
||||
ol = this._length,
|
||||
nl = this._length = ol + sjcl.bitArray.bitLength(data);
|
||||
for (i = 512+ol & -512; i <= nl; i+= 512) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Complete hashing and output the hash value.
|
||||
* @return {bitArray} The hash value, an array of 8 big-endian words.
|
||||
*/
|
||||
finalize:function () {
|
||||
var i, b = this._buffer, h = this._h;
|
||||
|
||||
// Round out and push the buffer
|
||||
b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]);
|
||||
|
||||
// Round out the buffer to a multiple of 16 words, less the 2 length words.
|
||||
for (i = b.length + 2; i & 15; i++) {
|
||||
b.push(0);
|
||||
}
|
||||
|
||||
// append the length
|
||||
b.push(Math.floor(this._length / 0x100000000));
|
||||
b.push(this._length | 0);
|
||||
|
||||
while (b.length) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
|
||||
this.reset();
|
||||
return h;
|
||||
},
|
||||
|
||||
/**
|
||||
* The SHA-256 initialization vector, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_init:[],
|
||||
/*
|
||||
_init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],
|
||||
*/
|
||||
|
||||
/**
|
||||
* The SHA-256 hash key, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_key:[],
|
||||
/*
|
||||
_key:
|
||||
[0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Function to precompute _init and _key.
|
||||
* @private
|
||||
*/
|
||||
_precompute: function () {
|
||||
var i = 0, prime = 2, factor;
|
||||
|
||||
function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; }
|
||||
|
||||
outer: for (; i<64; prime++) {
|
||||
for (factor=2; factor*factor <= prime; factor++) {
|
||||
if (prime % factor === 0) {
|
||||
// not a prime
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if (i<8) {
|
||||
this._init[i] = frac(Math.pow(prime, 1/2));
|
||||
}
|
||||
this._key[i] = frac(Math.pow(prime, 1/3));
|
||||
i++;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform one cycle of SHA-256.
|
||||
* @param {bitArray} words one block of words.
|
||||
* @private
|
||||
*/
|
||||
_block:function (words) {
|
||||
var i, tmp, a, b,
|
||||
w = words.slice(0),
|
||||
h = this._h,
|
||||
k = this._key,
|
||||
h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3],
|
||||
h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7];
|
||||
|
||||
/* Rationale for placement of |0 :
|
||||
* If a value can overflow is original 32 bits by a factor of more than a few
|
||||
* million (2^23 ish), there is a possibility that it might overflow the
|
||||
* 53-bit mantissa and lose precision.
|
||||
*
|
||||
* To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that
|
||||
* propagates around the loop, and on the hash state h[]. I don't believe
|
||||
* that the clamps on h4 and on h0 are strictly necessary, but it's close
|
||||
* (for h4 anyway), and better safe than sorry.
|
||||
*
|
||||
* The clamps on h[] are necessary for the output to be correct even in the
|
||||
* common case and for short inputs.
|
||||
*/
|
||||
for (i=0; i<64; i++) {
|
||||
// load up the input word for this round
|
||||
if (i<16) {
|
||||
tmp = w[i];
|
||||
} else {
|
||||
a = w[(i+1 ) & 15];
|
||||
b = w[(i+14) & 15];
|
||||
tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) +
|
||||
(b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) +
|
||||
w[i&15] + w[(i+9) & 15]) | 0;
|
||||
}
|
||||
|
||||
tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0;
|
||||
|
||||
// shift register
|
||||
h7 = h6; h6 = h5; h5 = h4;
|
||||
h4 = h3 + tmp | 0;
|
||||
h3 = h2; h2 = h1; h1 = h0;
|
||||
|
||||
h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0;
|
||||
}
|
||||
|
||||
h[0] = h[0]+h0 | 0;
|
||||
h[1] = h[1]+h1 | 0;
|
||||
h[2] = h[2]+h2 | 0;
|
||||
h[3] = h[3]+h3 | 0;
|
||||
h[4] = h[4]+h4 | 0;
|
||||
h[5] = h[5]+h5 | 0;
|
||||
h[6] = h[6]+h6 | 0;
|
||||
h[7] = h[7]+h7 | 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @fileOverview HMAC implementation.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** HMAC with the specified hash function.
|
||||
* @constructor
|
||||
* @param {bitArray} key the key for HMAC.
|
||||
* @param {Object} [hash=sjcl.hash.sha256] The hash function to use.
|
||||
*/
|
||||
sjcl.misc.hmac = function (key, Hash) {
|
||||
this._hash = Hash = Hash || sjcl.hash.sha256;
|
||||
var exKey = [[],[]], i,
|
||||
bs = Hash.prototype.blockSize / 32;
|
||||
this._baseHash = [new Hash(), new Hash()];
|
||||
|
||||
if (key.length > bs) {
|
||||
key = Hash.hash(key);
|
||||
}
|
||||
|
||||
for (i=0; i<bs; i++) {
|
||||
exKey[0][i] = key[i]^0x36363636;
|
||||
exKey[1][i] = key[i]^0x5C5C5C5C;
|
||||
}
|
||||
|
||||
this._baseHash[0].update(exKey[0]);
|
||||
this._baseHash[1].update(exKey[1]);
|
||||
this._resultHash = new Hash(this._baseHash[0]);
|
||||
};
|
||||
|
||||
/** HMAC with the specified hash function. Also called encrypt since it's a prf.
|
||||
* @param {bitArray|String} data The data to mac.
|
||||
*/
|
||||
sjcl.misc.hmac.prototype.encrypt = sjcl.misc.hmac.prototype.mac = function (data) {
|
||||
if (!this._updated) {
|
||||
this.update(data);
|
||||
return this.digest(data);
|
||||
} else {
|
||||
throw new sjcl.exception.invalid("encrypt on already updated hmac called!");
|
||||
}
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.reset = function () {
|
||||
this._resultHash = new this._hash(this._baseHash[0]);
|
||||
this._updated = false;
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.update = function (data) {
|
||||
this._updated = true;
|
||||
this._resultHash.update(data);
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.digest = function () {
|
||||
var w = this._resultHash.finalize(), result = new (this._hash)(this._baseHash[1]).update(w).finalize();
|
||||
|
||||
this.reset();
|
||||
|
||||
return result;
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
var PREFIX_NAME = 'identity.mozilla.com/picl/v1/';
|
||||
var bitSlice = sjcl.bitArray.bitSlice;
|
||||
var salt = sjcl.codec.hex.toBits('');
|
||||
|
||||
/**
|
||||
* hkdf - The HMAC-based Key Derivation Function
|
||||
* based on https://github.com/mozilla/node-hkdf
|
||||
*
|
||||
* @class hkdf
|
||||
* @param {bitArray} ikm Initial keying material
|
||||
* @param {bitArray} info Key derivation data
|
||||
* @param {bitArray} salt Salt
|
||||
* @param {integer} length Length of the derived key in bytes
|
||||
* @return promise object- It will resolve with `output` data
|
||||
*/
|
||||
function hkdf(ikm, info, salt, length, callback) {
|
||||
var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
|
||||
mac.update(ikm);
|
||||
|
||||
// compute the PRK
|
||||
var prk = mac.digest();
|
||||
|
||||
// hash length is 32 because only sjcl.hash.sha256 is used at this moment
|
||||
var hashLength = 32;
|
||||
var num_blocks = Math.ceil(length / hashLength);
|
||||
var prev = sjcl.codec.hex.toBits('');
|
||||
var output = '';
|
||||
|
||||
for (var i = 0; i < num_blocks; i++) {
|
||||
var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
|
||||
|
||||
var input = sjcl.bitArray.concat(
|
||||
sjcl.bitArray.concat(prev, info),
|
||||
sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1)))
|
||||
);
|
||||
|
||||
hmac.update(input);
|
||||
|
||||
prev = hmac.digest();
|
||||
output += sjcl.codec.hex.fromBits(prev);
|
||||
}
|
||||
|
||||
var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8);
|
||||
|
||||
callback(truncated);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @class hawkCredentials
|
||||
* @method deriveHawkCredentials
|
||||
* @param {String} tokenHex
|
||||
* @param {String} context
|
||||
* @param {int} size
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function deriveHawkCredentials(tokenHex, context, size, callback) {
|
||||
var token = sjcl.codec.hex.toBits(tokenHex);
|
||||
var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context);
|
||||
|
||||
hkdf(token, info, salt, size || 3 * 32, function(out) {
|
||||
var authKey = bitSlice(out, 8 * 32, 8 * 64);
|
||||
var bundleKey = bitSlice(out, 8 * 64);
|
||||
callback({
|
||||
algorithm: 'sha256',
|
||||
id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)),
|
||||
key: sjcl.codec.hex.fromBits(authKey),
|
||||
bundleKey: bundleKey
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -11,20 +11,17 @@ browser.jar:
|
||||
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
|
||||
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
|
||||
content/browser/loop/shared/img/loading-icon.gif (content/shared/img/loading-icon.gif)
|
||||
content/browser/loop/shared/js/client.js (content/shared/js/client.js)
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
|
||||
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
|
||||
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
|
||||
content/browser/loop/shared/libs/sjcl-dev20140604.js (content/shared/libs/sjcl-dev20140604.js)
|
||||
content/browser/loop/shared/libs/token.js (content/shared/libs/token.js)
|
||||
content/browser/loop/shared/libs/hawk-browser-2.2.1.js (content/shared/libs/hawk-browser-2.2.1.js)
|
||||
content/browser/loop/shared/sounds/Firefox-Long.ogg (content/shared/sounds/Firefox-Long.ogg)
|
||||
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
|
||||
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
|
||||
content/browser/loop/js/client.js (content/js/client.js)
|
||||
content/browser/loop/js/conversation.js (content/js/conversation.js)
|
||||
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
|
||||
content/browser/loop/js/panel.js (content/js/panel.js)
|
||||
# Partner SDK assets
|
||||
content/browser/loop/libs/sdk.js (content/libs/sdk.js)
|
||||
|
@ -30,10 +30,10 @@
|
||||
|
||||
<!-- app scripts -->
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
<script type="text/javascript" src="shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
||||
<script>
|
||||
|
@ -0,0 +1,123 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.StandaloneClient = (function($) {
|
||||
"use strict";
|
||||
|
||||
// The expected properties to be returned from the POST /calls request.
|
||||
var expectedCallsProperties = [ "sessionId", "sessionToken", "apiKey" ];
|
||||
|
||||
/**
|
||||
* Loop server standalone client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function StandaloneClient(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
StandaloneClient.prototype = {
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length == 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param jqXHR See jQuery docs
|
||||
* @param textStatus See jQuery docs
|
||||
* @param errorThrown See jQuery docs
|
||||
*/
|
||||
_failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
|
||||
var error = "Unknown error.",
|
||||
jsonRes = jqXHR && jqXHR.responseJSON || {};
|
||||
// Received error response format:
|
||||
// { "status": "errors",
|
||||
// "errors": [{
|
||||
// "location": "url",
|
||||
// "name": "token",
|
||||
// "description": "invalid token"
|
||||
// }]}
|
||||
if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
|
||||
error = "Details: " + jsonRes.errors.map(function(err) {
|
||||
return Object.keys(err).map(function(field) {
|
||||
return field + ": " + err[field];
|
||||
}).join(", ");
|
||||
}).join("; ");
|
||||
}
|
||||
var message = "HTTP " + jqXHR.status + " " + errorThrown +
|
||||
"; " + error;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts a call request to the server for a call represented by the
|
||||
* loopToken. Will return the session data for the call.
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {String} callType The type of media in the call, e.g.
|
||||
* "audio" or "audio-video"
|
||||
* @param {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, callType, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var req = $.ajax({
|
||||
url: this.settings.baseServerUrl + "/calls/" + loopToken,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify({callType: callType})
|
||||
});
|
||||
|
||||
req.done(function(sessionData) {
|
||||
try {
|
||||
cb(null, this._validate(sessionData, expectedCallsProperties));
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
};
|
||||
|
||||
return StandaloneClient;
|
||||
})(jQuery);
|
@ -92,8 +92,13 @@ loop.webapp = (function($, _, OT) {
|
||||
initiate: function(event) {
|
||||
event.preventDefault();
|
||||
this.model.initiate({
|
||||
baseServerUrl: baseServerUrl,
|
||||
outgoing: true
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl,
|
||||
}),
|
||||
outgoing: true,
|
||||
// For now, we assume both audio and video as there is no
|
||||
// other option to select.
|
||||
callType: "audio-video"
|
||||
});
|
||||
this.disableForm();
|
||||
}
|
||||
|
184
browser/components/loop/test/desktop-local/client_test.js
Normal file
184
browser/components/loop/test/desktop-local/client_test.js
Normal file
@ -0,0 +1,184 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.Client", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
callback,
|
||||
client,
|
||||
mozLoop,
|
||||
fakeToken,
|
||||
hawkRequestStub;
|
||||
|
||||
var fakeErrorRes = {
|
||||
code: 400,
|
||||
errno: 400,
|
||||
error: "Request Failed",
|
||||
message: "invalid token"
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
mozLoop = {
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken),
|
||||
ensureRegistered: sinon.stub().callsArgWith(0, null),
|
||||
noteCallUrlExpiry: sinon.spy(),
|
||||
hawkRequest: sinon.stub()
|
||||
};
|
||||
// Alias for clearer tests.
|
||||
hawkRequestStub = mozLoop.hawkRequest;
|
||||
client = new loop.Client({
|
||||
mozLoop: mozLoop
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.Client", function() {
|
||||
describe("#requestCallUrl", function() {
|
||||
it("should ensure loop is registered", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.ensureRegistered);
|
||||
});
|
||||
|
||||
it("should send an error when registration fails", function() {
|
||||
mozLoop.ensureRegistered.callsArgWith(0, "offline");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, "offline");
|
||||
});
|
||||
|
||||
it("should post to /call-url/", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(hawkRequestStub);
|
||||
sinon.assert.calledWith(hawkRequestStub,
|
||||
"/call-url/", "POST", {callerId: "foo"});
|
||||
});
|
||||
|
||||
it("should call the callback with the url when the request succeeds", function() {
|
||||
var callUrlData = {
|
||||
"callUrl": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
// 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.calledWithExactly(callback, null, callUrlData);
|
||||
});
|
||||
|
||||
it("should note the call url expiry when the request succeeds", function() {
|
||||
var callUrlData = {
|
||||
"callUrl": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// expiresAt is in hours, and noteCallUrlExpiry wants seconds.
|
||||
sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
|
||||
sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
|
||||
60 * 60 * 60);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
// Sets up the hawkRequest stub to trigger the callback with
|
||||
// an error
|
||||
hawkRequestStub.callsArgWith(3, null, "{}");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallsInfo", function() {
|
||||
it("should prevent launching a conversation when version is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallsInfo();
|
||||
}).to.Throw(Error, /missing required parameter version/);
|
||||
});
|
||||
|
||||
it("should perform a get on /calls", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledOnce(hawkRequestStub);
|
||||
sinon.assert.calledWith(hawkRequestStub,
|
||||
"/calls?version=42", "GET", null);
|
||||
|
||||
});
|
||||
|
||||
it("should request data for all calls", function() {
|
||||
hawkRequestStub.callsArgWith(3, null,
|
||||
'{"calls": [{"apiKey": "fake"}]}');
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
hawkRequestStub.callsArgWith(3, fakeErrorRes);
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
hawkRequestStub.callsArgWith(3, null, "{}");
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -31,15 +31,16 @@
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/client.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/panel.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="client_test.js"></script>
|
||||
<script src="conversation_test.js"></script>
|
||||
<script src="panel_test.js"></script>
|
||||
<script>
|
||||
|
@ -9,7 +9,8 @@ var expect = chai.expect;
|
||||
describe("loop.panel", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop;
|
||||
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop,
|
||||
fakeSeenToSPref = 0;
|
||||
|
||||
function createTestRouter(fakeDocument) {
|
||||
return new loop.panel.PanelRouter({
|
||||
@ -45,8 +46,16 @@ describe("loop.panel", function() {
|
||||
},
|
||||
get locale() {
|
||||
return "en-US";
|
||||
},
|
||||
setLoopCharPref: sandbox.stub(),
|
||||
getLoopCharPref: function () {
|
||||
if (fakeSeenToSPref === 0) {
|
||||
return null;
|
||||
}
|
||||
return 'seen';
|
||||
}
|
||||
};
|
||||
|
||||
document.mozL10n.initialize(navigator.mozLoop);
|
||||
});
|
||||
|
||||
@ -210,7 +219,7 @@ describe("loop.panel", function() {
|
||||
|
||||
describe("#getCallUrl", function() {
|
||||
it("should reset all pending notifications", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
|
||||
@ -220,7 +229,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
it("should request a call url to the server", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
sandbox.stub(view, "getNickname").returns("foo");
|
||||
@ -232,7 +241,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
it("should set the call url form in a pending state", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
||||
|
||||
@ -248,7 +257,7 @@ describe("loop.panel", function() {
|
||||
sandbox.stub(loop.panel.PanelView.prototype,
|
||||
"clearPending");
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
@ -260,7 +269,7 @@ describe("loop.panel", function() {
|
||||
|
||||
it("should notify the user when the operation failed", function() {
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
@ -278,7 +287,7 @@ describe("loop.panel", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
callUrlData = {
|
||||
call_url: "http://call.me/",
|
||||
callUrl: "http://call.me/",
|
||||
expiresAt: 1000
|
||||
};
|
||||
});
|
||||
@ -322,6 +331,58 @@ describe("loop.panel", function() {
|
||||
|
||||
sinon.assert.calledOnce(renderDnD);
|
||||
});
|
||||
|
||||
it("should render a ToSView", function() {
|
||||
var renderToS = sandbox.stub(loop.panel.ToSView.prototype, "render");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.render();
|
||||
|
||||
sinon.assert.calledOnce(renderToS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loop.panel.ToSView', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
$('#fixtures').append('<div id="#tos-view"></div>');
|
||||
|
||||
});
|
||||
|
||||
// XXX Until it's possible to easily test creation of text,
|
||||
// not doing so. As it stands, the magic in the L10nView
|
||||
// class makes stubbing BaseView.render impractical.
|
||||
|
||||
it("should set the value of the loop.seenToS preference to 'seen'",
|
||||
function() {
|
||||
var ToSView = new loop.panel.ToSView({el: $("#tos-view")});
|
||||
|
||||
ToSView.render();
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.setLoopCharPref);
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopCharPref,
|
||||
'seenToS', 'seen');
|
||||
});
|
||||
|
||||
it("should render when the value of loop.seenToS is not set", function() {
|
||||
var renderToS = sandbox.spy(loop.panel.ToSView.prototype, "render");
|
||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
||||
|
||||
ToSView.render();
|
||||
|
||||
sinon.assert.calledOnce(renderToS);
|
||||
});
|
||||
|
||||
it("should not render when the value of loop.seenToS is set to 'seen'",
|
||||
function() {
|
||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
||||
fakeSeenToSPref = 1;
|
||||
|
||||
ToSView.render();
|
||||
|
||||
sinon.assert.notCalled(navigator.mozLoop.setLoopCharPref);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,291 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.shared.Client", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
mozLoop,
|
||||
fakeToken;
|
||||
|
||||
var fakeErrorRes = JSON.stringify({
|
||||
status: "errors",
|
||||
errors: [{
|
||||
location: "url",
|
||||
name: "token",
|
||||
description: "invalid token"
|
||||
}]
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.shared.Client", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.shared.Client();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallUrl", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
window.navigator.mozLoop = {
|
||||
ensureRegistered: sinon.stub().callsArgWith(0, null),
|
||||
noteCallUrlExpiry: sinon.spy(),
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: window.navigator.mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should ensure loop is registered", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.ensureRegistered);
|
||||
});
|
||||
|
||||
it("should send an error when registration fails", function() {
|
||||
navigator.mozLoop.ensureRegistered.callsArgWith(0, "offline");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, "offline");
|
||||
});
|
||||
|
||||
it("should post to /call-url/", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/call-url/");
|
||||
expect(requests[0].requestBody).to.be.equal('callerId=foo');
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
client._credentials = {
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should request a call url", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, callUrlData);
|
||||
});
|
||||
|
||||
it("should note the call url expiry", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
// expiresAt is in hours, and noteCallUrlExpiry wants seconds.
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
|
||||
60 * 60 * 60);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallsInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
mozLoop = {
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when version is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallsInfo();
|
||||
}).to.Throw(Error, /missing required parameter version/);
|
||||
});
|
||||
|
||||
it("should request data for all calls", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls?version=42");
|
||||
expect(requests[0].method).to.be.equal("GET");
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"calls": [{"apiKey": "fake"}]}');
|
||||
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
client._credentials = {
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallsInfo("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: undefined}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -19,10 +19,7 @@
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script src="../../content/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script src="../../content/shared/libs/token.js"></script>
|
||||
<script src="../../content/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
<script src="../../standalone/content/libs/webl10n-20130617.js"></script>
|
||||
<script src="../../standalone/content/libs/webl10n-20130617.js"></script>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<script src="vendor/mocha-1.17.1.js"></script>
|
||||
@ -35,13 +32,11 @@
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="client_test.js"></script>
|
||||
<script src="models_test.js"></script>
|
||||
<script src="views_test.js"></script>
|
||||
<script src="router_test.js"></script>
|
||||
|
@ -52,68 +52,74 @@ describe("loop.shared.models", function() {
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var conversation, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
|
||||
var conversation, fakeClient, fakeBaseServerUrl,
|
||||
requestCallInfoStub, requestCallsInfoStub;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
||||
reqCallInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallInfo");
|
||||
reqCallsInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallsInfo");
|
||||
fakeClient = {
|
||||
requestCallInfo: sandbox.stub(),
|
||||
requestCallsInfo: sandbox.stub()
|
||||
};
|
||||
requestCallInfoStub = fakeClient.requestCallInfo;
|
||||
requestCallsInfoStub = fakeClient.requestCallsInfo;
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
it("call requestCallInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
client: fakeClient,
|
||||
outgoing: true,
|
||||
callType: "audio"
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallInfoStub);
|
||||
sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
|
||||
sinon.assert.calledOnce(requestCallInfoStub);
|
||||
sinon.assert.calledWith(requestCallInfoStub, "fakeToken", "audio");
|
||||
});
|
||||
|
||||
it("should not call requestCallsInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
client: fakeClient,
|
||||
outgoing: true,
|
||||
callType: "audio"
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallsInfoStub);
|
||||
sinon.assert.notCalled(requestCallsInfoStub);
|
||||
});
|
||||
|
||||
it("call requestCallsInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.set("loopVersion", 42);
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallsInfoStub);
|
||||
sinon.assert.calledWith(reqCallsInfoStub);
|
||||
sinon.assert.calledOnce(requestCallsInfoStub);
|
||||
sinon.assert.calledWith(requestCallsInfoStub, 42);
|
||||
});
|
||||
|
||||
it("should not call requestCallInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallInfoStub);
|
||||
sinon.assert.notCalled(requestCallInfoStub);
|
||||
});
|
||||
|
||||
it("should update conversation session information from server data",
|
||||
function() {
|
||||
sandbox.stub(conversation, "setReady");
|
||||
reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
|
||||
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
|
||||
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
@ -122,14 +128,14 @@ describe("loop.shared.models", function() {
|
||||
});
|
||||
|
||||
it("should trigger a `session:error` on failure", function(done) {
|
||||
reqCallInfoStub.callsArgWith(1,
|
||||
requestCallInfoStub.callsArgWith(2,
|
||||
new Error("failed: HTTP 400 Bad Request; fake"));
|
||||
|
||||
conversation.on("session:error", function(err) {
|
||||
expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
|
||||
done();
|
||||
}).initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
});
|
||||
|
@ -33,8 +33,10 @@
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneClient.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<!-- Test scripts -->
|
||||
<script src="standalone_client_test.js"></script>
|
||||
<script src="webapp_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
|
@ -0,0 +1,112 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
fakeToken;
|
||||
|
||||
var fakeErrorRes = JSON.stringify({
|
||||
status: "errors",
|
||||
errors: [{
|
||||
location: "url",
|
||||
name: "token",
|
||||
description: "invalid token"
|
||||
}]
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.StandaloneClient();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].requestBody).to.be.equal('{"callType":"audio"}');
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio-video", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -281,10 +281,11 @@ describe("loop.webapp", function() {
|
||||
|
||||
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
|
||||
sinon.assert.calledOnce(initiate);
|
||||
sinon.assert.calledWith(initiate, {
|
||||
baseServerUrl: loop.webapp.baseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
sinon.assert.calledWith(initiate, sinon.match(function (value) {
|
||||
return !!value.outgoing &&
|
||||
(value.client instanceof loop.StandaloneClient) &&
|
||||
value.client.settings.baseServerUrl === loop.webapp.baseServerUrl;
|
||||
}, "{client: <properly constructed client>, outgoing: true}"));
|
||||
});
|
||||
|
||||
it("should disable current form once session is initiated", function() {
|
||||
|
@ -359,7 +359,7 @@
|
||||
#ifdef MOZ_UPDATER
|
||||
<groupbox id="updateApp" align="start">
|
||||
<caption><label>&updateApp.label;</label></caption>
|
||||
<radiogroup id="updateRadioGroup"
|
||||
<radiogroup id="updateRadioGroup" align="start"
|
||||
oncommand="gAdvancedPane.updateWritePrefs();">
|
||||
#ifdef XP_WIN
|
||||
#ifdef MOZ_METRO
|
||||
|
@ -151,28 +151,37 @@
|
||||
<vbox id="historyCustomPane">
|
||||
<separator class="thin"/>
|
||||
<vbox class="indent">
|
||||
<checkbox id="privateBrowsingAutoStart"
|
||||
label="&privateBrowsingPermanent2.label;"
|
||||
accesskey="&privateBrowsingPermanent2.accesskey;"
|
||||
preference="browser.privatebrowsing.autostart"
|
||||
oncommand="gPrivacyPane.updateAutostart()"/>
|
||||
|
||||
<hbox>
|
||||
<checkbox id="privateBrowsingAutoStart"
|
||||
label="&privateBrowsingPermanent2.label;"
|
||||
accesskey="&privateBrowsingPermanent2.accesskey;"
|
||||
preference="browser.privatebrowsing.autostart"
|
||||
oncommand="gPrivacyPane.updateAutostart()"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
<vbox class="indent">
|
||||
<checkbox id="rememberHistory"
|
||||
label="&rememberHistory2.label;"
|
||||
accesskey="&rememberHistory2.accesskey;"
|
||||
preference="places.history.enabled"/>
|
||||
<checkbox id="rememberForms"
|
||||
label="&rememberSearchForm.label;"
|
||||
accesskey="&rememberSearchForm.accesskey;"
|
||||
preference="browser.formfill.enable"/>
|
||||
<hbox>
|
||||
<checkbox id="rememberHistory"
|
||||
label="&rememberHistory2.label;"
|
||||
accesskey="&rememberHistory2.accesskey;"
|
||||
preference="places.history.enabled"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
<hbox>
|
||||
<checkbox id="rememberForms"
|
||||
label="&rememberSearchForm.label;"
|
||||
accesskey="&rememberSearchForm.accesskey;"
|
||||
preference="browser.formfill.enable"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
|
||||
<hbox id="cookiesBox">
|
||||
<checkbox id="acceptCookies" label="&acceptCookies.label;" flex="1"
|
||||
<checkbox id="acceptCookies" label="&acceptCookies.label;"
|
||||
preference="network.cookie.cookieBehavior"
|
||||
accesskey="&acceptCookies.accesskey;"
|
||||
onsyncfrompreference="return gPrivacyPane.readAcceptCookies();"
|
||||
onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="cookieExceptions" oncommand="gPrivacyPane.showCookieExceptions();"
|
||||
label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
|
||||
preference="pref.privacy.disable_button.cookie_exceptions"/>
|
||||
@ -214,10 +223,11 @@
|
||||
</hbox>
|
||||
<hbox id="clearDataBox"
|
||||
align="center">
|
||||
<checkbox id="alwaysClear" flex="1"
|
||||
<checkbox id="alwaysClear"
|
||||
preference="privacy.sanitize.sanitizeOnShutdown"
|
||||
label="&clearOnClose.label;"
|
||||
accesskey="&clearOnClose.accesskey;"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="clearDataSettings" label="&clearOnCloseSettings.label;"
|
||||
accesskey="&clearOnCloseSettings.accesskey;"
|
||||
oncommand="gPrivacyPane.showClearPrivateDataSettings();"/>
|
||||
|
@ -276,7 +276,7 @@
|
||||
<groupbox id="syncOptions">
|
||||
<caption><label>&syncBrand.shortName.label;</label></caption>
|
||||
<hbox id="fxaSyncEngines">
|
||||
<vbox>
|
||||
<vbox align="start">
|
||||
<checkbox label="&engine.tabs.label;"
|
||||
accesskey="&engine.tabs.accesskey;"
|
||||
preference="engine.tabs"/>
|
||||
|
@ -14,7 +14,8 @@ function ifTestingSupported() {
|
||||
yield front.setup({
|
||||
tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
|
||||
startRecording: true,
|
||||
performReload: true
|
||||
performReload: true,
|
||||
storeCalls: true
|
||||
});
|
||||
ok(true, "The front was setup up successfully.");
|
||||
|
||||
|
@ -124,6 +124,8 @@ browser.jar:
|
||||
content/browser/devtools/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
|
||||
content/browser/devtools/spectrum-frame.xhtml (shared/widgets/spectrum-frame.xhtml)
|
||||
content/browser/devtools/spectrum.css (shared/widgets/spectrum.css)
|
||||
content/browser/devtools/cubic-bezier-frame.xhtml (shared/widgets/cubic-bezier-frame.xhtml)
|
||||
content/browser/devtools/cubic-bezier.css (shared/widgets/cubic-bezier.css)
|
||||
content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul)
|
||||
content/browser/devtools/eyedropper/crosshairs.css (eyedropper/crosshairs.css)
|
||||
content/browser/devtools/eyedropper/nocursor.css (eyedropper/nocursor.css)
|
||||
|
@ -1944,6 +1944,19 @@ function NetworkDetailsView() {
|
||||
};
|
||||
|
||||
NetworkDetailsView.prototype = {
|
||||
/**
|
||||
* An object containing the state of tabs.
|
||||
*/
|
||||
_viewState: {
|
||||
// if updating[tab] is true a task is currently updating the given tab.
|
||||
updating: [],
|
||||
// if dirty[tab] is true, the tab needs to be repopulated once current
|
||||
// update task finishes
|
||||
dirty: [],
|
||||
// the most recently received attachment data for the request
|
||||
latestData: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialization function, called when the network monitor is started.
|
||||
*/
|
||||
@ -2049,7 +2062,19 @@ NetworkDetailsView.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
let viewState = this._viewState;
|
||||
if (viewState.updating[tab]) {
|
||||
// A task is currently updating this tab. If we started another update
|
||||
// task now it would result in a duplicated content as described in bugs
|
||||
// 997065 and 984687. As there's no way to stop the current task mark the
|
||||
// tab dirty and refresh the panel once the current task finishes.
|
||||
viewState.dirty[tab] = true;
|
||||
viewState.latestData = src;
|
||||
return;
|
||||
}
|
||||
|
||||
Task.spawn(function*() {
|
||||
viewState.updating[tab] = true;
|
||||
switch (tab) {
|
||||
case 0: // "Headers"
|
||||
yield view._setSummary(src);
|
||||
@ -2079,13 +2104,32 @@ NetworkDetailsView.prototype = {
|
||||
yield view._setHtmlPreview(src.responseContent);
|
||||
break;
|
||||
}
|
||||
populated[tab] = true;
|
||||
window.emit(EVENTS.TAB_UPDATED);
|
||||
viewState.updating[tab] = false;
|
||||
}).then(() => {
|
||||
if (tab == this.widget.selectedIndex) {
|
||||
if (viewState.dirty[tab]) {
|
||||
// The request information was updated while the task was running.
|
||||
viewState.dirty[tab] = false;
|
||||
view.populate(viewState.latestData);
|
||||
}
|
||||
else {
|
||||
// Tab is selected but not dirty. We're done here.
|
||||
populated[tab] = true;
|
||||
window.emit(EVENTS.TAB_UPDATED);
|
||||
|
||||
if (NetMonitorController.isConnected()) {
|
||||
NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
|
||||
if (NetMonitorController.isConnected()) {
|
||||
NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
else {
|
||||
if (viewState.dirty[tab]) {
|
||||
// Tab is dirty but no longer selected. Don't refresh it now, it'll be
|
||||
// done if the tab is shown again.
|
||||
viewState.dirty[tab] = false;
|
||||
}
|
||||
}
|
||||
}, Cu.reportError);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -52,6 +52,7 @@ support-files =
|
||||
[browser_net_copy_as_curl.js]
|
||||
[browser_net_cyrillic-01.js]
|
||||
[browser_net_cyrillic-02.js]
|
||||
[browser_net_details-no-duplicated-content.js]
|
||||
[browser_net_filter-01.js]
|
||||
[browser_net_filter-02.js]
|
||||
[browser_net_filter-03.js]
|
||||
|
@ -0,0 +1,112 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// A test to ensure that the content in details pane is not duplicated.
|
||||
|
||||
let test = Task.async(function* () {
|
||||
info("Initializing test");
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let panel = monitor.panelWin;
|
||||
let { NetMonitorView, EVENTS } = panel;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
|
||||
let TEST_CASES = [
|
||||
{
|
||||
desc: "Test headers tab",
|
||||
pageURI: CUSTOM_GET_URL,
|
||||
isPost: false,
|
||||
tabIndex: 0,
|
||||
variablesView: NetworkDetails._headers,
|
||||
expectedScopeLength: 2,
|
||||
},
|
||||
{
|
||||
desc: "Test cookies tab",
|
||||
pageURI: CUSTOM_GET_URL,
|
||||
isPost: false,
|
||||
tabIndex: 1,
|
||||
variablesView: NetworkDetails._cookies,
|
||||
expectedScopeLength: 1,
|
||||
},
|
||||
{
|
||||
desc: "Test params tab",
|
||||
pageURI: POST_RAW_URL,
|
||||
isPost: true,
|
||||
tabIndex: 2,
|
||||
variablesView: NetworkDetails._params,
|
||||
expectedScopeLength: 1,
|
||||
},
|
||||
];
|
||||
|
||||
info("Adding a cookie for the \"Cookie\" tab test");
|
||||
debuggee.document.cookie = "a=b; Max-Age=10; path=" + CUSTOM_GET_URL;
|
||||
is(debuggee.document.cookie, "a=b", "Cookie was added.")
|
||||
|
||||
info("Running tests");
|
||||
for (let spec of TEST_CASES) {
|
||||
yield runTestCase(spec);
|
||||
}
|
||||
|
||||
// Remove the cookie. If an error occurs Max-Age ensures it doesn't stay to
|
||||
// mess with the tests.
|
||||
info("Removing the added cookie.");
|
||||
debuggee.document.cookie = "a=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
is(debuggee.document.cookie, "", "Cookie was removed.");
|
||||
|
||||
yield teardown(monitor);
|
||||
finish();
|
||||
|
||||
/**
|
||||
* A helper that handles the execution of each case.
|
||||
*/
|
||||
function* runTestCase(spec) {
|
||||
info("Running case: " + spec.desc);
|
||||
debuggee.content.location = spec.pageURI;
|
||||
|
||||
yield waitForNetworkEvents(monitor, 1);
|
||||
RequestsMenu.clear();
|
||||
yield waitForFinalDetailTabUpdate(spec.tabIndex, spec.isPost);
|
||||
|
||||
is(spec.variablesView._store.length, spec.expectedScopeLength,
|
||||
"View contains " + spec.expectedScopeLength + " scope headers");
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that prepares the variables view for the actual testing. It
|
||||
* - selects the correct tab
|
||||
* - performs the specified request
|
||||
* - opens the details view
|
||||
* - waits for the final update to happen
|
||||
*/
|
||||
function* waitForFinalDetailTabUpdate(tabIndex, isPost) {
|
||||
let onNetworkEvent = waitFor(panel, EVENTS.NETWORK_EVENT);
|
||||
let onDetailsPopulated = waitFor(panel, EVENTS.NETWORKDETAILSVIEW_POPULATED);
|
||||
let onRequestFinished = isPost ?
|
||||
waitForNetworkEvents(monitor, 0, 1) : waitForNetworkEvents(monitor, 1);
|
||||
|
||||
info("Performing a request");
|
||||
debuggee.performRequests(1, null);
|
||||
|
||||
info("Waiting for NETWORK_EVENT");
|
||||
yield onNetworkEvent;
|
||||
|
||||
ok(true, "Received NETWORK_EVENT. Selecting the item.");
|
||||
let item = RequestsMenu.getItemAtIndex(0);
|
||||
RequestsMenu.selectedItem = item;
|
||||
|
||||
info("Item selected. Waiting for NETWORKDETAILSVIEW_POPULATED");
|
||||
yield onDetailsPopulated;
|
||||
|
||||
info("Selecting tab at index " + tabIndex);
|
||||
NetworkDetails.widget.selectedIndex = tabIndex;
|
||||
|
||||
ok(true, "Received NETWORKDETAILSVIEW_POPULATED. Waiting for request to finish");
|
||||
yield onRequestFinished;
|
||||
|
||||
ok(true, "Request finished. Waiting for tab update to complete");
|
||||
let onDetailsUpdateFinished = waitFor(panel, EVENTS.TAB_UPDATED);
|
||||
yield onDetailsUpdateFinished;
|
||||
ok(true, "Details were updated");
|
||||
}
|
||||
});
|
@ -1,155 +1,203 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests if requests display the correct status code and text in the UI.
|
||||
*/
|
||||
|
||||
function test() {
|
||||
initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
let test = Task.async(function*() {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(STATUS_CODES_URL);
|
||||
|
||||
let { document, L10N, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
info("Starting test... ");
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
NetworkDetails._params.lazyEmpty = false;
|
||||
let { document, L10N, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
let requestItems = [];
|
||||
|
||||
waitForNetworkEvents(aMonitor, 5).then(() => {
|
||||
let requestItems = [];
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
NetworkDetails._params.lazyEmpty = false;
|
||||
|
||||
verifyRequestItemTarget(requestItems[0] = RequestsMenu.getItemAtIndex(0),
|
||||
"GET", STATUS_CODES_SJS + "?sts=100", {
|
||||
status: 101,
|
||||
statusText: "Switching Protocols",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
|
||||
time: true
|
||||
});
|
||||
verifyRequestItemTarget(requestItems[1] = RequestsMenu.getItemAtIndex(1),
|
||||
"GET", STATUS_CODES_SJS + "?sts=200", {
|
||||
status: 202,
|
||||
statusText: "Created",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
});
|
||||
verifyRequestItemTarget(requestItems[2] = RequestsMenu.getItemAtIndex(2),
|
||||
"GET", STATUS_CODES_SJS + "?sts=300", {
|
||||
status: 303,
|
||||
statusText: "See Other",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
|
||||
time: true
|
||||
});
|
||||
verifyRequestItemTarget(requestItems[3] = RequestsMenu.getItemAtIndex(3),
|
||||
"GET", STATUS_CODES_SJS + "?sts=400", {
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
});
|
||||
verifyRequestItemTarget(requestItems[4] = RequestsMenu.getItemAtIndex(4),
|
||||
"GET", STATUS_CODES_SJS + "?sts=500", {
|
||||
status: 501,
|
||||
statusText: "Not Implemented",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
});
|
||||
|
||||
// Test summaries...
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll("#details-pane tab")[0]);
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
|
||||
testSummary("GET", STATUS_CODES_SJS + "?sts=100", "101", "Switching Protocols");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
|
||||
testSummary("GET", STATUS_CODES_SJS + "?sts=200", "202", "Created");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
|
||||
testSummary("GET", STATUS_CODES_SJS + "?sts=300", "303", "See Other");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
|
||||
testSummary("GET", STATUS_CODES_SJS + "?sts=400", "404", "Not Found");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
|
||||
testSummary("GET", STATUS_CODES_SJS + "?sts=500", "501", "Not Implemented");
|
||||
|
||||
// Test params...
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll("#details-pane tab")[2]);
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
|
||||
testParamsTab("\"100\"");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
|
||||
testParamsTab("\"200\"");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
|
||||
testParamsTab("\"300\"");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
|
||||
testParamsTab("\"400\"");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
|
||||
testParamsTab("\"500\"");
|
||||
|
||||
// We're done here.
|
||||
teardown(aMonitor).then(finish);
|
||||
|
||||
function testSummary(aMethod, aUrl, aStatus, aStatusText) {
|
||||
let tab = document.querySelectorAll("#details-pane tab")[0];
|
||||
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
|
||||
|
||||
is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
|
||||
aUrl, "The url summary value is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
|
||||
aMethod, "The method summary value is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
|
||||
aStatus, "The status summary code is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
|
||||
aStatus + " " + aStatusText, "The status summary value is incorrect.");
|
||||
const REQUEST_DATA = [
|
||||
{ // request #0
|
||||
method: "GET",
|
||||
uri: STATUS_CODES_SJS + "?sts=100",
|
||||
details: {
|
||||
status: 101,
|
||||
statusText: "Switching Protocols",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
|
||||
time: true
|
||||
}
|
||||
|
||||
function testParamsTab(aStatusParamValue) {
|
||||
let tab = document.querySelectorAll("#details-pane tab")[2];
|
||||
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
|
||||
|
||||
is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
|
||||
"There should be 1 param scope displayed in this tabpanel.");
|
||||
is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
|
||||
"There should be 1 param value displayed in this tabpanel.");
|
||||
is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
|
||||
"The empty notice should not be displayed in this tabpanel.");
|
||||
|
||||
let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
|
||||
|
||||
is(paramsScope.querySelector(".name").getAttribute("value"),
|
||||
L10N.getStr("paramsQueryString"),
|
||||
"The params scope doesn't have the correct title.");
|
||||
|
||||
is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
|
||||
"sts", "The param name was incorrect.");
|
||||
is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
|
||||
aStatusParamValue, "The param value was incorrect.");
|
||||
|
||||
is(tabpanel.querySelector("#request-params-box")
|
||||
.hasAttribute("hidden"), false,
|
||||
"The request params box should not be hidden.");
|
||||
is(tabpanel.querySelector("#request-post-data-textarea-box")
|
||||
.hasAttribute("hidden"), true,
|
||||
"The request post data textarea box should be hidden.");
|
||||
},
|
||||
{ // request #1
|
||||
method: "GET",
|
||||
uri: STATUS_CODES_SJS + "?sts=200",
|
||||
details: {
|
||||
status: 202,
|
||||
statusText: "Created",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
}
|
||||
});
|
||||
},
|
||||
{ // request #2
|
||||
method: "GET",
|
||||
uri: STATUS_CODES_SJS + "?sts=300",
|
||||
details: {
|
||||
status: 303,
|
||||
statusText: "See Other",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
|
||||
time: true
|
||||
}
|
||||
},
|
||||
{ // request #3
|
||||
method: "GET",
|
||||
uri: STATUS_CODES_SJS + "?sts=400",
|
||||
details: {
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
}
|
||||
},
|
||||
{ // request #4
|
||||
method: "GET",
|
||||
uri: STATUS_CODES_SJS + "?sts=500",
|
||||
details: {
|
||||
status: 501,
|
||||
statusText: "Not Implemented",
|
||||
type: "plain",
|
||||
fullMimeType: "text/plain; charset=utf-8",
|
||||
size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
|
||||
time: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
aDebuggee.performRequests();
|
||||
});
|
||||
}
|
||||
debuggee.performRequests();
|
||||
yield waitForNetworkEvents(monitor, 5);
|
||||
|
||||
info("Performing tests");
|
||||
yield verifyRequests();
|
||||
yield testTab(0, testSummary);
|
||||
yield testTab(2, testParams);
|
||||
|
||||
yield teardown(monitor);
|
||||
finish();
|
||||
|
||||
/**
|
||||
* A helper that verifies all requests show the correct information and caches
|
||||
* RequestsMenu items to requestItems array.
|
||||
*/
|
||||
function* verifyRequests() {
|
||||
info("Verifying requests contain correct information.");
|
||||
let index = 0;
|
||||
for (let request of REQUEST_DATA) {
|
||||
let item = RequestsMenu.getItemAtIndex(index);
|
||||
requestItems[index] = item;
|
||||
|
||||
info("Verifying request #" + index);
|
||||
yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that opens a given tab of request details pane, selects and passes
|
||||
* all requests to the given test function.
|
||||
*
|
||||
* @param Number tab
|
||||
* The index of NetworkDetails tab to activate.
|
||||
* @param Function testFn(requestItem)
|
||||
* A function that should perform all necessary tests. It's called once
|
||||
* for every item of REQUEST_DATA with that item being selected in the
|
||||
* NetworkMonitor.
|
||||
*/
|
||||
function* testTab(tab, testFn) {
|
||||
info("Testing tab #" + tab);
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll("#details-pane tab")[tab]);
|
||||
|
||||
let counter = 0;
|
||||
for (let item of REQUEST_DATA) {
|
||||
info("Waiting tab #" + tab + " to update with request #" + counter);
|
||||
yield chooseRequest(counter);
|
||||
|
||||
info("Tab updated. Performing checks");
|
||||
yield testFn(item);
|
||||
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that tests "Summary" contains correct information.
|
||||
*/
|
||||
function* testSummary(data) {
|
||||
let tab = document.querySelectorAll("#details-pane tab")[0];
|
||||
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
|
||||
|
||||
let { method, uri, details: { status, statusText } } = data;
|
||||
is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
|
||||
uri, "The url summary value is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
|
||||
method, "The method summary value is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
|
||||
status, "The status summary code is incorrect.");
|
||||
is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
|
||||
status + " " + statusText, "The status summary value is incorrect.");
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that tests "Params" tab contains correct information.
|
||||
*/
|
||||
function* testParams(data) {
|
||||
let tab = document.querySelectorAll("#details-pane tab")[2];
|
||||
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
|
||||
let statusParamValue = data.uri.split("=").pop();
|
||||
let statusParamShownValue = "\"" + statusParamValue + "\"";
|
||||
|
||||
is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
|
||||
"There should be 1 param scope displayed in this tabpanel.");
|
||||
is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
|
||||
"There should be 1 param value displayed in this tabpanel.");
|
||||
is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
|
||||
"The empty notice should not be displayed in this tabpanel.");
|
||||
|
||||
let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
|
||||
|
||||
is(paramsScope.querySelector(".name").getAttribute("value"),
|
||||
L10N.getStr("paramsQueryString"),
|
||||
"The params scope doesn't have the correct title.");
|
||||
|
||||
is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
|
||||
"sts", "The param name was incorrect.");
|
||||
is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
|
||||
statusParamShownValue, "The param value was incorrect.");
|
||||
|
||||
is(tabpanel.querySelector("#request-params-box")
|
||||
.hasAttribute("hidden"), false,
|
||||
"The request params box should not be hidden.");
|
||||
is(tabpanel.querySelector("#request-post-data-textarea-box")
|
||||
.hasAttribute("hidden"), true,
|
||||
"The request post data textarea box should be hidden.");
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that clicks on a specified request and returns a promise resolved
|
||||
* when NetworkDetails has been populated with the data of the given request.
|
||||
*/
|
||||
function chooseRequest(index) {
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].target);
|
||||
return waitFor(monitor.panelWin, monitor.panelWin.EVENTS.TAB_UPDATED);
|
||||
}
|
||||
});
|
||||
|
@ -12,6 +12,9 @@ support-files =
|
||||
leakhunt.js
|
||||
|
||||
[browser_css_color.js]
|
||||
[browser_cubic-bezier-01.js]
|
||||
[browser_cubic-bezier-02.js]
|
||||
[browser_cubic-bezier-03.js]
|
||||
[browser_graphs-01.js]
|
||||
[browser_graphs-02.js]
|
||||
[browser_graphs-03.js]
|
||||
|
36
browser/devtools/shared/test/browser_cubic-bezier-01.js
Normal file
36
browser/devtools/shared/test/browser_cubic-bezier-01.js
Normal file
@ -0,0 +1,36 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that the CubicBezierWidget generates content in a given parent node
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab(TEST_URI);
|
||||
|
||||
info("Checking that the markup is created in the parent");
|
||||
let container = content.document.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container);
|
||||
|
||||
ok(container.querySelector(".coordinate-plane"),
|
||||
"The coordinate plane has been added");
|
||||
let buttons = container.querySelectorAll("button");
|
||||
is(buttons.length, 2,
|
||||
"The 2 control points have been added");
|
||||
is(buttons[0].className, "control-point");
|
||||
is(buttons[0].id, "P1");
|
||||
is(buttons[1].className, "control-point");
|
||||
is(buttons[1].id, "P2");
|
||||
ok(container.querySelector("canvas"), "The curve canvas has been added");
|
||||
|
||||
info("Destroying the widget");
|
||||
w.destroy();
|
||||
is(container.children.length, 0, "All nodes have been removed");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
152
browser/devtools/shared/test/browser_cubic-bezier-02.js
Normal file
152
browser/devtools/shared/test/browser_cubic-bezier-02.js
Normal file
@ -0,0 +1,152 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the CubicBezierWidget events
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget, PREDEFINED} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab(TEST_URI);
|
||||
|
||||
let container = content.document.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container, PREDEFINED.linear);
|
||||
|
||||
yield pointsCanBeDragged(w);
|
||||
yield curveCanBeClicked(w);
|
||||
yield pointsCanBeMovedWithKeyboard(w);
|
||||
|
||||
w.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* pointsCanBeDragged(widget) {
|
||||
info("Checking that the control points can be dragged with the mouse");
|
||||
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Generating a mousedown/move/up on P1");
|
||||
widget._onPointMouseDown({target: widget.p1});
|
||||
EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
|
||||
{type: "mousemove"}, content.window);
|
||||
EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
|
||||
{type: "mouseup"}, content.window);
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The widget fired the updated event");
|
||||
ok(bezier, "The updated event contains a bezier argument");
|
||||
is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Generating a mousedown/move/up on P2");
|
||||
widget._onPointMouseDown({target: widget.p2});
|
||||
EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
|
||||
{type: "mousemove"}, content.window);
|
||||
EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
|
||||
{type: "mouseup"}, content.window);
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
|
||||
}
|
||||
|
||||
function* curveCanBeClicked(widget) {
|
||||
info("Checking that clicking on the curve moves the closest control point");
|
||||
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Click close to P1");
|
||||
widget._onCurveClick({pageX: 50, pageY: 150});
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The widget fired the updated event");
|
||||
is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
|
||||
is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
|
||||
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Click close to P2");
|
||||
widget._onCurveClick({pageX: 150, pageY: 250});
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
|
||||
is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
|
||||
is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
|
||||
}
|
||||
|
||||
function* pointsCanBeMovedWithKeyboard(widget) {
|
||||
info("Checking that points respond to keyboard events");
|
||||
|
||||
info("Moving P1 to the left");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the left, fast");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the right, fast");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the bottom");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the bottom, fast");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the top, fast");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Checking that keyboard events also work with P2");
|
||||
info("Moving P2 to the left");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
|
||||
}
|
||||
|
||||
function getKeyEvent(target, keyCode, shift=false) {
|
||||
return {
|
||||
target: target,
|
||||
keyCode: keyCode,
|
||||
shiftKey: shift,
|
||||
preventDefault: () => {}
|
||||
};
|
||||
}
|
67
browser/devtools/shared/test/browser_cubic-bezier-03.js
Normal file
67
browser/devtools/shared/test/browser_cubic-bezier-03.js
Normal file
@ -0,0 +1,67 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that coordinates can be changed programatically in the CubicBezierWidget
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget, PREDEFINED} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab(TEST_URI);
|
||||
|
||||
let container = content.document.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container, PREDEFINED.linear);
|
||||
|
||||
yield coordinatesCanBeChangedByProvidingAnArray(w);
|
||||
yield coordinatesCanBeChangedByProvidingAValue(w);
|
||||
|
||||
w.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* coordinatesCanBeChangedByProvidingAnArray(widget) {
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Setting new coordinates");
|
||||
widget.coordinates = [0,1,1,0];
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The updated event was fired as a result of setting coordinates");
|
||||
|
||||
is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
|
||||
}
|
||||
|
||||
function* coordinatesCanBeChangedByProvidingAValue(widget) {
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Setting linear css value");
|
||||
widget.cssCubicBezierValue = "linear";
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The updated event was fired as a result of setting cssValue");
|
||||
|
||||
is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
|
||||
|
||||
info("Setting a custom cubic-bezier css value");
|
||||
let onUpdated = widget.once("updated");
|
||||
widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.45)";
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The updated event was fired as a result of setting cssValue");
|
||||
|
||||
is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], -.5, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 1.45, "The new P2 progress coordinate is correct");
|
||||
}
|
113
browser/devtools/shared/test/unit/test_bezierCanvas.js
Normal file
113
browser/devtools/shared/test/unit/test_bezierCanvas.js
Normal file
@ -0,0 +1,113 @@
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the BezierCanvas API in the CubicBezierWidget module
|
||||
|
||||
const Cu = Components.utils;
|
||||
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let require = devtools.require;
|
||||
let {CubicBezier, BezierCanvas} = require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
function run_test() {
|
||||
offsetsGetterReturnsData();
|
||||
convertsOffsetsToCoordinates();
|
||||
plotsCanvas();
|
||||
}
|
||||
|
||||
function offsetsGetterReturnsData() {
|
||||
do_print("offsets getter returns an array of 2 offset objects");
|
||||
|
||||
let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
|
||||
let offsets = b.offsets;
|
||||
|
||||
do_check_eq(offsets.length, 2);
|
||||
|
||||
do_check_true("top" in offsets[0]);
|
||||
do_check_true("left" in offsets[0]);
|
||||
do_check_true("top" in offsets[1]);
|
||||
do_check_true("left" in offsets[1]);
|
||||
|
||||
do_check_eq(offsets[0].top, "300px");
|
||||
do_check_eq(offsets[0].left, "0px");
|
||||
do_check_eq(offsets[1].top, "100px");
|
||||
do_check_eq(offsets[1].left, "200px");
|
||||
|
||||
do_print("offsets getter returns data according to current padding");
|
||||
|
||||
let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
|
||||
let offsets = b.offsets;
|
||||
|
||||
do_check_eq(offsets[0].top, "400px");
|
||||
do_check_eq(offsets[0].left, "0px");
|
||||
do_check_eq(offsets[1].top, "0px");
|
||||
do_check_eq(offsets[1].left, "200px");
|
||||
}
|
||||
|
||||
function convertsOffsetsToCoordinates() {
|
||||
do_print("Converts offsets to coordinates");
|
||||
|
||||
let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
|
||||
|
||||
let coordinates = b.offsetsToCoordinates({style: {
|
||||
left: "0px",
|
||||
top: "0px"
|
||||
}});
|
||||
do_check_eq(coordinates.length, 2);
|
||||
do_check_eq(coordinates[0], 0);
|
||||
do_check_eq(coordinates[1], 1.5);
|
||||
|
||||
let coordinates = b.offsetsToCoordinates({style: {
|
||||
left: "0px",
|
||||
top: "300px"
|
||||
}});
|
||||
do_check_eq(coordinates[0], 0);
|
||||
do_check_eq(coordinates[1], 0);
|
||||
|
||||
let coordinates = b.offsetsToCoordinates({style: {
|
||||
left: "200px",
|
||||
top: "100px"
|
||||
}});
|
||||
do_check_eq(coordinates[0], 1);
|
||||
do_check_eq(coordinates[1], 1);
|
||||
}
|
||||
|
||||
function plotsCanvas() {
|
||||
do_print("Plots the curve to the canvas");
|
||||
|
||||
let hasDrawnCurve = false;
|
||||
let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
|
||||
b.ctx.bezierCurveTo = () => hasDrawnCurve = true;
|
||||
b.plot();
|
||||
|
||||
do_check_true(hasDrawnCurve);
|
||||
}
|
||||
|
||||
function getCubicBezier() {
|
||||
return new CubicBezier([0,0,1,1]);
|
||||
}
|
||||
|
||||
function getCanvasMock(w=200, h=400) {
|
||||
return {
|
||||
getContext: function() {
|
||||
return {
|
||||
scale: () => {},
|
||||
translate: () => {},
|
||||
clearRect: () => {},
|
||||
beginPath: () => {},
|
||||
closePath: () => {},
|
||||
moveTo: () => {},
|
||||
lineTo: () => {},
|
||||
stroke: () => {},
|
||||
arc: () => {},
|
||||
fill: () => {},
|
||||
bezierCurveTo: () => {}
|
||||
};
|
||||
},
|
||||
width: w,
|
||||
height: h
|
||||
};
|
||||
}
|
102
browser/devtools/shared/test/unit/test_cubicBezier.js
Normal file
102
browser/devtools/shared/test/unit/test_cubicBezier.js
Normal file
@ -0,0 +1,102 @@
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the CubicBezier API in the CubicBezierWidget module
|
||||
|
||||
const Cu = Components.utils;
|
||||
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let require = devtools.require;
|
||||
let {CubicBezier} = require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
function run_test() {
|
||||
throwsWhenMissingCoordinates();
|
||||
throwsWhenIncorrectCoordinates();
|
||||
convertsStringCoordinates();
|
||||
coordinatesToStringOutputsAString();
|
||||
pointGettersReturnPointCoordinatesArrays();
|
||||
toStringOutputsCubicBezierValue();
|
||||
}
|
||||
|
||||
function throwsWhenMissingCoordinates() {
|
||||
do_check_throws(() => {
|
||||
new CubicBezier();
|
||||
}, "Throws an exception when coordinates are missing");
|
||||
}
|
||||
|
||||
function throwsWhenIncorrectCoordinates() {
|
||||
do_check_throws(() => {
|
||||
new CubicBezier([]);
|
||||
}, "Throws an exception when coordinates are incorrect (empty array)");
|
||||
|
||||
do_check_throws(() => {
|
||||
new CubicBezier([0,0]);
|
||||
}, "Throws an exception when coordinates are incorrect (incomplete array)");
|
||||
|
||||
do_check_throws(() => {
|
||||
new CubicBezier(["a", "b", "c", "d"]);
|
||||
}, "Throws an exception when coordinates are incorrect (invalid type)");
|
||||
|
||||
do_check_throws(() => {
|
||||
new CubicBezier([1.5, 0, 1.5, 0]);
|
||||
}, "Throws an exception when coordinates are incorrect (time range invalid)");
|
||||
|
||||
do_check_throws(() => {
|
||||
new CubicBezier([-0.5, 0, -0.5, 0]);
|
||||
}, "Throws an exception when coordinates are incorrect (time range invalid)");
|
||||
}
|
||||
|
||||
function convertsStringCoordinates() {
|
||||
do_print("Converts string coordinates to numbers");
|
||||
let c = new CubicBezier(["0", "1", ".5", "-2"]);
|
||||
|
||||
do_check_eq(c.coordinates[0], 0);
|
||||
do_check_eq(c.coordinates[1], 1);
|
||||
do_check_eq(c.coordinates[2], .5);
|
||||
do_check_eq(c.coordinates[3], -2);
|
||||
}
|
||||
|
||||
function coordinatesToStringOutputsAString() {
|
||||
do_print("coordinates.toString() outputs a string representation");
|
||||
|
||||
let c = new CubicBezier(["0", "1", "0.5", "-2"]);
|
||||
let string = c.coordinates.toString();
|
||||
do_check_eq(string, "0,1,.5,-2");
|
||||
|
||||
let c = new CubicBezier([1, 1, 1, 1]);
|
||||
let string = c.coordinates.toString();
|
||||
do_check_eq(string, "1,1,1,1");
|
||||
}
|
||||
|
||||
function pointGettersReturnPointCoordinatesArrays() {
|
||||
do_print("Points getters return arrays of coordinates");
|
||||
|
||||
let c = new CubicBezier([0, .2, .5, 1]);
|
||||
do_check_eq(c.P1[0], 0);
|
||||
do_check_eq(c.P1[1], .2);
|
||||
do_check_eq(c.P2[0], .5);
|
||||
do_check_eq(c.P2[1], 1);
|
||||
}
|
||||
|
||||
function toStringOutputsCubicBezierValue() {
|
||||
do_print("toString() outputs the cubic-bezier() value");
|
||||
|
||||
let c = new CubicBezier([0, 0, 1, 1]);
|
||||
do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
|
||||
}
|
||||
|
||||
function do_check_throws(cb, info) {
|
||||
do_print(info);
|
||||
|
||||
let hasThrown = false;
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
hasThrown = true;
|
||||
}
|
||||
|
||||
do_check_true(hasThrown);
|
||||
}
|
@ -3,4 +3,6 @@ head =
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_bezierCanvas.js]
|
||||
[test_cubicBezier.js]
|
||||
[test_undoStack.js]
|
||||
|
556
browser/devtools/shared/widgets/CubicBezierWidget.js
Normal file
556
browser/devtools/shared/widgets/CubicBezierWidget.js
Normal file
@ -0,0 +1,556 @@
|
||||
/**
|
||||
* Copyright (c) 2013 Lea Verou. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Based on www.cubic-bezier.com by Lea Verou
|
||||
// See https://github.com/LeaVerou/cubic-bezier
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
const {setTimeout, clearTimeout} = require("sdk/timers");
|
||||
|
||||
const PREDEFINED = exports.PREDEFINED = {
|
||||
"ease": [.25, .1, .25, 1],
|
||||
"linear": [0, 0, 1, 1],
|
||||
"ease-in": [.42, 0, 1, 1],
|
||||
"ease-out": [0, 0, .58, 1],
|
||||
"ease-in-out": [.42, 0, .58, 1]
|
||||
};
|
||||
|
||||
/**
|
||||
* CubicBezier data structure helper
|
||||
* Accepts an array of coordinates and exposes a few useful getters
|
||||
* @param {Array} coordinates i.e. [.42, 0, .58, 1]
|
||||
*/
|
||||
function CubicBezier(coordinates) {
|
||||
if (!coordinates) {
|
||||
throw "No offsets were defined";
|
||||
}
|
||||
|
||||
this.coordinates = coordinates.map(n => +n);
|
||||
|
||||
for (let i = 4; i--;) {
|
||||
let xy = this.coordinates[i];
|
||||
if (isNaN(xy) || (!(i%2) && (xy < 0 || xy > 1))) {
|
||||
throw "Wrong coordinate at " + i + "(" + xy + ")";
|
||||
}
|
||||
}
|
||||
|
||||
this.coordinates.toString = function() {
|
||||
return this.map(n => {
|
||||
return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
|
||||
}) + "";
|
||||
}
|
||||
}
|
||||
|
||||
exports.CubicBezier = CubicBezier;
|
||||
|
||||
CubicBezier.prototype = {
|
||||
get P1() {
|
||||
return this.coordinates.slice(0, 2);
|
||||
},
|
||||
|
||||
get P2() {
|
||||
return this.coordinates.slice(2);
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return 'cubic-bezier(' + this.coordinates + ')';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bezier curve canvas plotting class
|
||||
* @param {DOMNode} canvas
|
||||
* @param {CubicBezier} bezier
|
||||
* @param {Array} padding Amount of horizontal,vertical padding around the graph
|
||||
*/
|
||||
function BezierCanvas(canvas, bezier, padding) {
|
||||
this.canvas = canvas;
|
||||
this.bezier = bezier;
|
||||
this.padding = getPadding(padding);
|
||||
|
||||
// Convert to a cartesian coordinate system with axes from 0 to 1
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
let p = this.padding;
|
||||
|
||||
this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
|
||||
-canvas.height * (1 - p[0] - p[2]));
|
||||
this.ctx.translate(p[3] / (1 - p[1] - p[3]),
|
||||
-1 - p[0] / (1 - p[0] - p[2]));
|
||||
};
|
||||
|
||||
exports.BezierCanvas = BezierCanvas;
|
||||
|
||||
BezierCanvas.prototype = {
|
||||
/**
|
||||
* Get P1 and P2 current top/left offsets so they can be positioned
|
||||
* @return {Array} Returns an array of 2 {top:String,left:String} objects
|
||||
*/
|
||||
get offsets() {
|
||||
let p = this.padding, w = this.canvas.width, h = this.canvas.height;
|
||||
|
||||
return [{
|
||||
left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px',
|
||||
top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px'
|
||||
}, {
|
||||
left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
|
||||
top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
|
||||
}]
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert an element's left/top offsets into coordinates
|
||||
*/
|
||||
offsetsToCoordinates: function(element) {
|
||||
let p = this.padding, w = this.canvas.width, h = this.canvas.height;
|
||||
|
||||
// Convert padding percentage to actual padding
|
||||
p = p.map(function(a, i) { return a * (i % 2? w : h)});
|
||||
|
||||
return [
|
||||
(parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
|
||||
(h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw the cubic bezier curve for the current coordinates
|
||||
*/
|
||||
plot: function(settings={}) {
|
||||
let xy = this.bezier.coordinates;
|
||||
|
||||
let defaultSettings = {
|
||||
handleColor: '#666',
|
||||
handleThickness: .008,
|
||||
bezierColor: '#4C9ED9',
|
||||
bezierThickness: .015
|
||||
};
|
||||
|
||||
for (let setting in settings) {
|
||||
defaultSettings[setting] = settings[setting];
|
||||
}
|
||||
|
||||
this.ctx.clearRect(-.5,-.5, 2, 2);
|
||||
|
||||
// Draw control handles
|
||||
this.ctx.beginPath();
|
||||
this.ctx.fillStyle = defaultSettings.handleColor;
|
||||
this.ctx.lineWidth = defaultSettings.handleThickness;
|
||||
this.ctx.strokeStyle = defaultSettings.handleColor;
|
||||
|
||||
this.ctx.moveTo(0, 0);
|
||||
this.ctx.lineTo(xy[0], xy[1]);
|
||||
this.ctx.moveTo(1,1);
|
||||
this.ctx.lineTo(xy[2], xy[3]);
|
||||
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
|
||||
function circle(ctx, cx, cy, r) {
|
||||
return ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
|
||||
this.ctx.fill();
|
||||
circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
|
||||
this.ctx.fill();
|
||||
|
||||
// Draw bezier curve
|
||||
this.ctx.beginPath();
|
||||
this.ctx.lineWidth = defaultSettings.bezierThickness;
|
||||
this.ctx.strokeStyle = defaultSettings.bezierColor;
|
||||
this.ctx.moveTo(0,0);
|
||||
this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1);
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
|
||||
* adds the control points and user interaction
|
||||
* @param {DOMNode} parent The container where the graph should be created
|
||||
* @param {Array} coordinates Coordinates of the curve to be drawn
|
||||
*
|
||||
* Emits "updated" events whenever the curve is changed. Along with the event is
|
||||
* sent a CubicBezier object
|
||||
*/
|
||||
function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
|
||||
this.parent = parent;
|
||||
let {curve, p1, p2} = this._initMarkup();
|
||||
|
||||
this.curve = curve;
|
||||
this.curveBoundingBox = curve.getBoundingClientRect();
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
|
||||
// Create and plot the bezier curve
|
||||
this.bezierCanvas = new BezierCanvas(this.curve,
|
||||
new CubicBezier(coordinates), [.25, 0]);
|
||||
this.bezierCanvas.plot();
|
||||
|
||||
// Place the control points
|
||||
let offsets = this.bezierCanvas.offsets;
|
||||
this.p1.style.left = offsets[0].left;
|
||||
this.p1.style.top = offsets[0].top;
|
||||
this.p2.style.left = offsets[1].left;
|
||||
this.p2.style.top = offsets[1].top;
|
||||
|
||||
this._onPointMouseDown = this._onPointMouseDown.bind(this);
|
||||
this._onPointKeyDown = this._onPointKeyDown.bind(this);
|
||||
this._onCurveClick = this._onCurveClick.bind(this);
|
||||
this._initEvents();
|
||||
|
||||
// Add the timing function previewer
|
||||
this.timingPreview = new TimingFunctionPreviewWidget(parent);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.CubicBezierWidget = CubicBezierWidget;
|
||||
|
||||
CubicBezierWidget.prototype = {
|
||||
_initMarkup: function() {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let plane = doc.createElement("div");
|
||||
plane.className = "coordinate-plane";
|
||||
|
||||
let p1 = doc.createElement("button");
|
||||
p1.className = "control-point";
|
||||
p1.id = "P1";
|
||||
plane.appendChild(p1);
|
||||
|
||||
let p2 = doc.createElement("button");
|
||||
p2.className = "control-point";
|
||||
p2.id = "P2";
|
||||
plane.appendChild(p2);
|
||||
|
||||
let curve = doc.createElement("canvas");
|
||||
curve.setAttribute("height", "400");
|
||||
curve.setAttribute("width", "200");
|
||||
curve.id = "curve";
|
||||
plane.appendChild(curve);
|
||||
|
||||
this.parent.appendChild(plane);
|
||||
|
||||
return {
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
curve: curve
|
||||
}
|
||||
},
|
||||
|
||||
_removeMarkup: function() {
|
||||
this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
|
||||
},
|
||||
|
||||
_initEvents: function() {
|
||||
this.p1.addEventListener("mousedown", this._onPointMouseDown);
|
||||
this.p2.addEventListener("mousedown", this._onPointMouseDown);
|
||||
|
||||
this.p1.addEventListener("keydown", this._onPointKeyDown);
|
||||
this.p2.addEventListener("keydown", this._onPointKeyDown);
|
||||
|
||||
this.curve.addEventListener("click", this._onCurveClick);
|
||||
},
|
||||
|
||||
_removeEvents: function() {
|
||||
this.p1.removeEventListener("mousedown", this._onPointMouseDown);
|
||||
this.p2.removeEventListener("mousedown", this._onPointMouseDown);
|
||||
|
||||
this.p1.removeEventListener("keydown", this._onPointKeyDown);
|
||||
this.p2.removeEventListener("keydown", this._onPointKeyDown);
|
||||
|
||||
this.curve.removeEventListener("click", this._onCurveClick);
|
||||
},
|
||||
|
||||
_onPointMouseDown: function(event) {
|
||||
// Updating the boundingbox in case it has changed
|
||||
this.curveBoundingBox = this.curve.getBoundingClientRect();
|
||||
|
||||
let point = event.target;
|
||||
let doc = point.ownerDocument;
|
||||
let self = this;
|
||||
|
||||
doc.onmousemove = function drag(e) {
|
||||
let x = e.pageX;
|
||||
let y = e.pageY;
|
||||
let left = self.curveBoundingBox.left;
|
||||
let top = self.curveBoundingBox.top;
|
||||
|
||||
if (x === 0 && y == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Constrain x
|
||||
x = Math.min(Math.max(left, x), left + self.curveBoundingBox.width);
|
||||
|
||||
point.style.left = x - left + "px";
|
||||
point.style.top = y - top + "px";
|
||||
|
||||
self._updateFromPoints();
|
||||
};
|
||||
|
||||
doc.onmouseup = function () {
|
||||
point.focus();
|
||||
doc.onmousemove = doc.onmouseup = null;
|
||||
}
|
||||
},
|
||||
|
||||
_onPointKeyDown: function(event) {
|
||||
let point = event.target;
|
||||
let code = event.keyCode;
|
||||
|
||||
if (code >= 37 && code <= 40) {
|
||||
event.preventDefault();
|
||||
|
||||
// Arrow keys pressed
|
||||
let left = parseInt(point.style.left);
|
||||
let top = parseInt(point.style.top);
|
||||
let offset = 3 * (event.shiftKey ? 10 : 1);
|
||||
|
||||
switch (code) {
|
||||
case 37: point.style.left = left - offset + 'px'; break;
|
||||
case 38: point.style.top = top - offset + 'px'; break;
|
||||
case 39: point.style.left = left + offset + 'px'; break;
|
||||
case 40: point.style.top = top + offset + 'px'; break;
|
||||
}
|
||||
|
||||
this._updateFromPoints();
|
||||
}
|
||||
},
|
||||
|
||||
_onCurveClick: function(event) {
|
||||
let left = this.curveBoundingBox.left;
|
||||
let top = this.curveBoundingBox.top;
|
||||
let x = event.pageX - left;
|
||||
let y = event.pageY - top;
|
||||
|
||||
// Find which point is closer
|
||||
let distP1 = distance(x, y,
|
||||
parseInt(this.p1.style.left), parseInt(this.p1.style.top));
|
||||
let distP2 = distance(x, y,
|
||||
parseInt(this.p2.style.left), parseInt(this.p2.style.top));
|
||||
|
||||
let point = distP1 < distP2 ? this.p1 : this.p2;
|
||||
point.style.left = x + "px";
|
||||
point.style.top = y + "px";
|
||||
|
||||
this._updateFromPoints();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current point coordinates and redraw the curve to match
|
||||
*/
|
||||
_updateFromPoints: function() {
|
||||
// Get the new coordinates from the point's offsets
|
||||
let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
|
||||
coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
|
||||
|
||||
this._redraw(coordinates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Redraw the curve
|
||||
* @param {Array} coordinates The array of control point coordinates
|
||||
*/
|
||||
_redraw: function(coordinates) {
|
||||
// Provide a new CubicBezier to the canvas and plot the curve
|
||||
this.bezierCanvas.bezier = new CubicBezier(coordinates);
|
||||
this.bezierCanvas.plot();
|
||||
this.emit("updated", this.bezierCanvas.bezier);
|
||||
|
||||
this.timingPreview.preview(this.bezierCanvas.bezier + "");
|
||||
},
|
||||
|
||||
/**
|
||||
* Set new coordinates for the control points and redraw the curve
|
||||
* @param {Array} coordinates
|
||||
*/
|
||||
set coordinates(coordinates) {
|
||||
this._redraw(coordinates)
|
||||
|
||||
// Move the points
|
||||
let offsets = this.bezierCanvas.offsets;
|
||||
this.p1.style.left = offsets[0].left;
|
||||
this.p1.style.top = offsets[0].top;
|
||||
this.p2.style.left = offsets[1].left;
|
||||
this.p2.style.top = offsets[1].top;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set new coordinates for the control point and redraw the curve
|
||||
* @param {String} value A string value. E.g. "linear", "cubic-bezier(0,0,1,1)"
|
||||
*/
|
||||
set cssCubicBezierValue(value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
// Try with one of the predefined values
|
||||
let coordinates = PREDEFINED[value];
|
||||
|
||||
// Otherwise parse the coordinates from the cubic-bezier function
|
||||
if (!coordinates && value.startsWith("cubic-bezier")) {
|
||||
coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
|
||||
}
|
||||
|
||||
this.coordinates = coordinates;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._removeEvents();
|
||||
this._removeMarkup();
|
||||
|
||||
this.timingPreview.destroy();
|
||||
|
||||
this.curve = this.p1 = this.p2 = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The TimingFunctionPreviewWidget animates a dot on a scale with a given
|
||||
* timing-function
|
||||
* @param {DOMNode} parent The container where this widget should go
|
||||
*/
|
||||
function TimingFunctionPreviewWidget(parent) {
|
||||
this.previousValue = null;
|
||||
this.autoRestartAnimation = null;
|
||||
|
||||
this.parent = parent;
|
||||
this._initMarkup();
|
||||
}
|
||||
|
||||
TimingFunctionPreviewWidget.prototype = {
|
||||
PREVIEW_DURATION: 1000,
|
||||
|
||||
_initMarkup: function() {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let container = doc.createElement("div");
|
||||
container.className = "timing-function-preview";
|
||||
|
||||
this.dot = doc.createElement("div");
|
||||
this.dot.className = "dot";
|
||||
container.appendChild(this.dot);
|
||||
|
||||
let scale = doc.createElement("div");
|
||||
scale.className = "scale";
|
||||
container.appendChild(scale);
|
||||
|
||||
this.parent.appendChild(container);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
clearTimeout(this.autoRestartAnimation);
|
||||
this.parent.querySelector(".timing-function-preview").remove();
|
||||
this.parent = this.dot = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Preview a new timing function. The current preview will only be stopped if
|
||||
* the supplied function value is different from the previous one. If the
|
||||
* supplied function is invalid, the preview will stop.
|
||||
* @param {String} value
|
||||
*/
|
||||
preview: function(value) {
|
||||
// Don't restart the preview animation if the value is the same
|
||||
if (value === this.previousValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearTimeout(this.autoRestartAnimation);
|
||||
|
||||
if (isValidTimingFunction(value)) {
|
||||
this.dot.style.animationTimingFunction = value;
|
||||
this.restartAnimation();
|
||||
}
|
||||
|
||||
this.previousValue = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-start the preview animation from the beginning
|
||||
*/
|
||||
restartAnimation: function() {
|
||||
// Reset the animation duration in case it was changed
|
||||
this.dot.style.animationDuration = (this.PREVIEW_DURATION * 2) + "ms";
|
||||
|
||||
// Just toggling the class won't do it unless there's a sync reflow
|
||||
this.dot.classList.remove("animate");
|
||||
let w = this.dot.offsetWidth;
|
||||
this.dot.classList.add("animate");
|
||||
|
||||
// Restart it again after a while
|
||||
this.autoRestartAnimation = setTimeout(this.restartAnimation.bind(this),
|
||||
this.PREVIEW_DURATION * 2);
|
||||
}
|
||||
};
|
||||
|
||||
// Helpers
|
||||
|
||||
function getPadding(padding) {
|
||||
let p = typeof padding === 'number'? [padding] : padding;
|
||||
|
||||
if (p.length === 1) {
|
||||
p[1] = p[0];
|
||||
}
|
||||
|
||||
if (p.length === 2) {
|
||||
p[2] = p[0];
|
||||
}
|
||||
|
||||
if (p.length === 3) {
|
||||
p[3] = p[1];
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid timing-function value
|
||||
* @param {String} value
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isValidTimingFunction(value) {
|
||||
// Either it's a predefined value
|
||||
if (value in PREDEFINED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Or it has to match a cubic-bezier expression
|
||||
if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
25
browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
Normal file
25
browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
|
||||
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 200px;
|
||||
height: 415px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body role="application">
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
142
browser/devtools/shared/widgets/cubic-bezier.css
Normal file
142
browser/devtools/shared/widgets/cubic-bezier.css
Normal file
@ -0,0 +1,142 @@
|
||||
/* 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/. */
|
||||
|
||||
/* Based on Lea Verou www.cubic-bezier.com
|
||||
See https://github.com/LeaVerou/cubic-bezier */
|
||||
|
||||
.coordinate-plane {
|
||||
position: absolute;
|
||||
line-height: 0;
|
||||
height: 400px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.coordinate-plane:before,
|
||||
.coordinate-plane:after {
|
||||
position: absolute;
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coordinate-plane:before {
|
||||
content: "";
|
||||
border-bottom: 2px solid;
|
||||
transform: rotate(-90deg) translateY(2px);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.coordinate-plane:after {
|
||||
content: "";
|
||||
border-top: 2px solid;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.theme-dark .coordinate-plane:before,
|
||||
.theme-dark .coordinate-plane:after {
|
||||
border-color: #eee;
|
||||
}
|
||||
|
||||
.control-point {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border: 0;
|
||||
background: #666;
|
||||
display: block;
|
||||
margin: -5px 0 0 -5px;
|
||||
outline: none;
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#P1x, #P1y {
|
||||
color: #f08;
|
||||
}
|
||||
|
||||
#P2x, #P2y {
|
||||
color: #0ab;
|
||||
}
|
||||
|
||||
canvas#curve {
|
||||
background:
|
||||
linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
|
||||
repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
|
||||
repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
|
||||
|
||||
background-size: 100% 50%, 100% 50%, 100% 50%;
|
||||
background-position: 25%, 0, 0;
|
||||
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.theme-dark canvas#curve {
|
||||
background:
|
||||
linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
|
||||
repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
|
||||
repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
|
||||
|
||||
background-size: 100% 50%, 100% 50%, 100% 50%;
|
||||
background-position: 25%, 0, 0;
|
||||
}
|
||||
|
||||
/* Timing function preview widget */
|
||||
|
||||
.timing-function-preview {
|
||||
position: absolute;
|
||||
top: 400px;
|
||||
}
|
||||
|
||||
.timing-function-preview .scale {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
width: 200px;
|
||||
height: 1px;
|
||||
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.timing-function-preview .dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -7px;
|
||||
z-index: 2;
|
||||
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
background: #4C9ED9;
|
||||
}
|
||||
|
||||
.timing-function-preview .dot.animate {
|
||||
animation-duration: 2.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-name: timing-function-preview;
|
||||
}
|
||||
|
||||
@keyframes timing-function-preview {
|
||||
0% {
|
||||
left: -7px;
|
||||
}
|
||||
33% {
|
||||
left: 193px;
|
||||
}
|
||||
50% {
|
||||
left: 193px;
|
||||
}
|
||||
83% {
|
||||
left: -7px;
|
||||
}
|
||||
100% {
|
||||
left: -7px;
|
||||
}
|
||||
}
|
@ -30,3 +30,8 @@ cannot_start_call_session_not_ready=Can't start call, session is not ready.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
|
||||
connection_error_see_console_notification=Call failed; see console for details.
|
||||
## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
|
||||
## part between {{..}}
|
||||
legal_text_and_links.innerHTML=By using this product you agree to the <a \
|
||||
href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||
href="{{privacy_notice_url}}">Privacy Notice</a>
|
||||
|
@ -2095,8 +2095,6 @@ public class GeckoAppShell
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true)
|
||||
public static Context getContext() {
|
||||
if (sContextGetter == null)
|
||||
return null;
|
||||
return sContextGetter.getContext();
|
||||
}
|
||||
|
||||
|
@ -102,8 +102,6 @@ public class GeckoApplication extends Application
|
||||
}
|
||||
|
||||
public void onActivityResume(GeckoActivityStatus activity) {
|
||||
GeckoAppShell.setContextGetter(this);
|
||||
|
||||
if (mPausedGecko) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createAppForegroundingEvent());
|
||||
mPausedGecko = false;
|
||||
|
@ -106,14 +106,14 @@ public final class HomeConfig {
|
||||
private final AuthConfig mAuthConfig;
|
||||
private final EnumSet<Flags> mFlags;
|
||||
|
||||
private static final String JSON_KEY_TYPE = "type";
|
||||
private static final String JSON_KEY_TITLE = "title";
|
||||
private static final String JSON_KEY_ID = "id";
|
||||
private static final String JSON_KEY_LAYOUT = "layout";
|
||||
private static final String JSON_KEY_VIEWS = "views";
|
||||
private static final String JSON_KEY_AUTH_CONFIG = "authConfig";
|
||||
private static final String JSON_KEY_DEFAULT = "default";
|
||||
private static final String JSON_KEY_DISABLED = "disabled";
|
||||
static final String JSON_KEY_TYPE = "type";
|
||||
static final String JSON_KEY_TITLE = "title";
|
||||
static final String JSON_KEY_ID = "id";
|
||||
static final String JSON_KEY_LAYOUT = "layout";
|
||||
static final String JSON_KEY_VIEWS = "views";
|
||||
static final String JSON_KEY_AUTH_CONFIG = "authConfig";
|
||||
static final String JSON_KEY_DEFAULT = "default";
|
||||
static final String JSON_KEY_DISABLED = "disabled";
|
||||
|
||||
public enum Flags {
|
||||
DEFAULT_PANEL,
|
||||
@ -616,14 +616,14 @@ public final class HomeConfig {
|
||||
private final EmptyViewConfig mEmptyViewConfig;
|
||||
private final EnumSet<Flags> mFlags;
|
||||
|
||||
private static final String JSON_KEY_TYPE = "type";
|
||||
private static final String JSON_KEY_DATASET = "dataset";
|
||||
private static final String JSON_KEY_ITEM_TYPE = "itemType";
|
||||
private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
|
||||
private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
|
||||
private static final String JSON_KEY_FILTER = "filter";
|
||||
private static final String JSON_KEY_EMPTY = "empty";
|
||||
private static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
|
||||
static final String JSON_KEY_TYPE = "type";
|
||||
static final String JSON_KEY_DATASET = "dataset";
|
||||
static final String JSON_KEY_ITEM_TYPE = "itemType";
|
||||
static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
|
||||
static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
|
||||
static final String JSON_KEY_FILTER = "filter";
|
||||
static final String JSON_KEY_EMPTY = "empty";
|
||||
static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
|
||||
|
||||
public enum Flags {
|
||||
REFRESH_ENABLED
|
||||
@ -818,8 +818,8 @@ public final class HomeConfig {
|
||||
private final String mText;
|
||||
private final String mImageUrl;
|
||||
|
||||
private static final String JSON_KEY_TEXT = "text";
|
||||
private static final String JSON_KEY_IMAGE_URL = "imageUrl";
|
||||
static final String JSON_KEY_TEXT = "text";
|
||||
static final String JSON_KEY_IMAGE_URL = "imageUrl";
|
||||
|
||||
public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
|
||||
mText = json.optString(JSON_KEY_TEXT, null);
|
||||
@ -888,9 +888,9 @@ public final class HomeConfig {
|
||||
private final String mButtonText;
|
||||
private final String mImageUrl;
|
||||
|
||||
private static final String JSON_KEY_MESSAGE_TEXT = "messageText";
|
||||
private static final String JSON_KEY_BUTTON_TEXT = "buttonText";
|
||||
private static final String JSON_KEY_IMAGE_URL = "imageUrl";
|
||||
static final String JSON_KEY_MESSAGE_TEXT = "messageText";
|
||||
static final String JSON_KEY_BUTTON_TEXT = "buttonText";
|
||||
static final String JSON_KEY_IMAGE_URL = "imageUrl";
|
||||
|
||||
public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException {
|
||||
mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT);
|
||||
|
@ -97,6 +97,22 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
|
||||
return new State(panelConfigs, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the panels to check if they are all disabled.
|
||||
*/
|
||||
private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
|
||||
final int count = jsonPanels.length();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
|
||||
|
||||
if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates JSON config data storage.
|
||||
*
|
||||
@ -144,8 +160,13 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
|
||||
switch (v) {
|
||||
case 1:
|
||||
// Add "Recent Tabs" panel
|
||||
final PanelConfig recentTabsConfig = createBuiltinPanelConfig(context, PanelType.RECENT_TABS);
|
||||
final JSONObject jsonRecentTabsConfig = recentTabsConfig.toJSON();
|
||||
final JSONObject jsonRecentTabsConfig =
|
||||
createBuiltinPanelConfig(context, PanelType.RECENT_TABS).toJSON();
|
||||
|
||||
// If any panel is enabled, then we should make the recent tabs
|
||||
// panel enabled.
|
||||
jsonRecentTabsConfig.put(PanelConfig.JSON_KEY_DISABLED,
|
||||
allPanelsAreDisabled(originalJsonPanels));
|
||||
|
||||
// Add the new panel to the front of the array on phones.
|
||||
if (!HardwareUtils.isTablet()) {
|
||||
|
@ -46,11 +46,10 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.mozilla.gecko.tabspanel.RemoteTabsPanel
|
||||
android:id="@+id/remote_tabs"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:visibility="gone"/>
|
||||
<ViewStub android:id="@+id/remote_tabs_panel_stub"
|
||||
android:layout="@layout/remote_tabs_panel_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</view>
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<org.mozilla.gecko.tabspanel.RemoteTabsPanel xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/remote_tabs_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
@ -45,12 +45,11 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.mozilla.gecko.tabspanel.RemoteTabsPanel
|
||||
android:id="@+id/remote_tabs"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ViewStub android:id="@+id/remote_tabs_panel_stub"
|
||||
android:layout="@layout/remote_tabs_panel_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</view>
|
||||
|
||||
</merge>
|
||||
|
@ -31,6 +31,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
@ -117,6 +118,7 @@ public class TabsPanel extends LinearLayout
|
||||
public void onResume() {
|
||||
if (mPanel == mPanelRemote) {
|
||||
// Refresh the remote panel.
|
||||
initializeRemotePanelView();
|
||||
mPanelRemote.show();
|
||||
}
|
||||
}
|
||||
@ -141,9 +143,6 @@ public class TabsPanel extends LinearLayout
|
||||
mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
|
||||
mPanelPrivate.setTabsPanel(this);
|
||||
|
||||
mPanelRemote = (PanelView) findViewById(R.id.remote_tabs);
|
||||
mPanelRemote.setTabsPanel(this);
|
||||
|
||||
mFooter = (RelativeLayout) findViewById(R.id.tabs_panel_footer);
|
||||
|
||||
mAddTab = (ImageButton) findViewById(R.id.add_tab);
|
||||
@ -407,6 +406,7 @@ public class TabsPanel extends LinearLayout
|
||||
mPanel = mPanelPrivate;
|
||||
break;
|
||||
case REMOTE_TABS:
|
||||
initializeRemotePanelView();
|
||||
mPanel = mPanelRemote;
|
||||
break;
|
||||
|
||||
@ -555,4 +555,11 @@ public class TabsPanel extends LinearLayout
|
||||
public void setIconDrawable(Panel panel, int resource) {
|
||||
mTabWidget.setIconDrawable(panel.ordinal(), resource);
|
||||
}
|
||||
|
||||
private void initializeRemotePanelView() {
|
||||
if (mPanelRemote == null) {
|
||||
mPanelRemote = (PanelView) ((ViewStub) findViewById(R.id.remote_tabs_panel_stub)).inflate();
|
||||
mPanelRemote.setTabsPanel(TabsPanel.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ var gToolboxChanged = false;
|
||||
var gToolboxSheet = false;
|
||||
var gPaletteBox = null;
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function onLoad()
|
||||
{
|
||||
if ("arguments" in window && window.arguments[0]) {
|
||||
@ -457,9 +459,7 @@ function setDragActive(aItem, aValue)
|
||||
|
||||
function addNewToolbar()
|
||||
{
|
||||
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Components.interfaces.nsIPromptService);
|
||||
|
||||
var promptService = Services.prompt;
|
||||
var stringBundle = document.getElementById("stringBundle");
|
||||
var message = stringBundle.getString("enterToolbarName");
|
||||
var title = stringBundle.getString("enterToolbarTitle");
|
||||
@ -819,6 +819,12 @@ function onPaletteDrop(aEvent)
|
||||
|
||||
|
||||
function isUnwantedDragEvent(aEvent) {
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("toolkit.customization.unsafe_drag_events")) {
|
||||
return false;
|
||||
}
|
||||
} catch (ex) {}
|
||||
|
||||
/* Discard drag events that originated from a separate window to
|
||||
prevent content->chrome privilege escalations. */
|
||||
let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
|
||||
|
@ -76,6 +76,7 @@
|
||||
<li><a href="about:license#cairo">Cairo Component Licenses</a></li>
|
||||
<li><a href="about:license#chromium">Chromium License</a></li>
|
||||
<li><a href="about:license#codemirror">CodeMirror License</a></li>
|
||||
<li><a href="about:license#cubic-bezier">cubic-bezier License</a></li>
|
||||
<li><a href="about:license#dtoa">dtoa License</a></li>
|
||||
<li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
|
||||
<li><a href="about:license#edl">Eclipse Distribution License</a></li>
|
||||
@ -2026,6 +2027,36 @@ licences.
|
||||
</pre>
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="cubic-bezier"></a>cubic-bezier License</h1>
|
||||
|
||||
<p>This license applies to the file
|
||||
<span class="path">browser/devtools/shared/widgets/CubicBezierWidget.js
|
||||
</span>.</p>
|
||||
<pre>
|
||||
Copyright (c) 2013 Lea Verou. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</pre>
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="dtoa"></a>dtoa License</h1>
|
||||
|
@ -492,6 +492,10 @@ function Requisition(options) {
|
||||
|
||||
addMapping(this);
|
||||
this._setBlankAssignment(this.commandAssignment);
|
||||
|
||||
// If a command calls context.update then the UI needs some way to be
|
||||
// informed of the change
|
||||
this.onExternalUpdate = util.createEvent('Requisition.onExternalUpdate');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -584,8 +588,8 @@ Object.defineProperty(Requisition.prototype, 'executionContext', {
|
||||
if (legacy) {
|
||||
this._executionContext.createView = view.createView;
|
||||
this._executionContext.exec = this.exec.bind(this);
|
||||
this._executionContext.update = this.update.bind(this);
|
||||
this._executionContext.updateExec = this.updateExec.bind(this);
|
||||
this._executionContext.update = this._contextUpdate.bind(this);
|
||||
this._executionContext.updateExec = this._contextUpdateExec.bind(this);
|
||||
|
||||
Object.defineProperty(this._executionContext, 'document', {
|
||||
get: function() { return requisition.document; },
|
||||
@ -612,8 +616,8 @@ Object.defineProperty(Requisition.prototype, 'conversionContext', {
|
||||
|
||||
createView: view.createView,
|
||||
exec: this.exec.bind(this),
|
||||
update: this.update.bind(this),
|
||||
updateExec: this.updateExec.bind(this)
|
||||
update: this._contextUpdate.bind(this),
|
||||
updateExec: this._contextUpdateExec.bind(this)
|
||||
};
|
||||
|
||||
// Alias requisition so we're clear about what's what
|
||||
@ -766,17 +770,36 @@ Requisition.prototype._getFirstBlankPositionalAssignment = function() {
|
||||
return reply;
|
||||
};
|
||||
|
||||
/**
|
||||
* The update process is asynchronous, so there is (unavoidably) a window
|
||||
* where we've worked out the command but don't yet understand all the params.
|
||||
* If we try to do things to a requisition in this window we may get
|
||||
* inconsistent results. Asynchronous promises have made the window bigger.
|
||||
* The only time we've seen this in practice is during focus events due to
|
||||
* clicking on a shortcut. The focus want to check the cursor position while
|
||||
* the shortcut is updating the command line.
|
||||
* This function allows us to detect and back out of this problem.
|
||||
* We should be able to remove this function when all the state in a
|
||||
* requisition can be encapsulated and updated atomically.
|
||||
*/
|
||||
Requisition.prototype.isUpToDate = function() {
|
||||
if (!this._args) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < this._args.length; i++) {
|
||||
if (this._args[i].assignment == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Look through the arguments attached to our assignments for the assignment
|
||||
* at the given position.
|
||||
* @param {number} cursor The cursor position to query
|
||||
*/
|
||||
Requisition.prototype.getAssignmentAt = function(cursor) {
|
||||
if (!this._args) {
|
||||
console.trace();
|
||||
throw new Error('Missing args');
|
||||
}
|
||||
|
||||
// We short circuit this one because we may have no args, or no args with
|
||||
// any size and the alg below only finds arguments with size.
|
||||
if (cursor === 0) {
|
||||
@ -822,14 +845,7 @@ Requisition.prototype.getAssignmentAt = function(cursor) {
|
||||
// Possible shortcut, we don't really need to go through all the args
|
||||
// to work out the solution to this
|
||||
|
||||
var reply = assignForPos[cursor - 1];
|
||||
|
||||
if (!reply) {
|
||||
throw new Error('Missing assignment.' +
|
||||
' cursor=' + cursor + ' text=' + this.toString());
|
||||
}
|
||||
|
||||
return reply;
|
||||
return assignForPos[cursor - 1];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1478,15 +1494,31 @@ function getDataCommandAttribute(element) {
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Designed to be called from context.update(). Acts just like update() except
|
||||
* that it also calls onExternalUpdate() to inform the UI of an unexpected
|
||||
* change to the current command.
|
||||
*/
|
||||
Requisition.prototype._contextUpdate = function(typed) {
|
||||
return this.update(typed).then(function(reply) {
|
||||
this.onExternalUpdate({ typed: typed });
|
||||
return reply;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by the UI when ever the user interacts with a command line input
|
||||
* @param typed The contents of the input field
|
||||
* @param typed The contents of the input field OR an HTML element (or an event
|
||||
* that targets an HTML element) which has a data-command attribute or a child
|
||||
* with the same that contains the command to update with
|
||||
*/
|
||||
Requisition.prototype.update = function(typed) {
|
||||
if (typeof HTMLElement !== 'undefined' && typed instanceof HTMLElement) {
|
||||
// Should be "if (typed instanceof HTMLElement)" except Gecko
|
||||
if (typeof typed.querySelector === 'function') {
|
||||
typed = getDataCommandAttribute(typed);
|
||||
}
|
||||
if (typeof Event !== 'undefined' && typed instanceof Event) {
|
||||
// Should be "if (typed instanceof Event)" except Gecko
|
||||
if (typeof typed.currentTarget === 'object') {
|
||||
typed = getDataCommandAttribute(typed.currentTarget);
|
||||
}
|
||||
|
||||
@ -2068,6 +2100,18 @@ Requisition.prototype.exec = function(options) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Designed to be called from context.updateExec(). Acts just like updateExec()
|
||||
* except that it also calls onExternalUpdate() to inform the UI of an
|
||||
* unexpected change to the current command.
|
||||
*/
|
||||
Requisition.prototype._contextUpdateExec = function(typed, options) {
|
||||
return this.updateExec(typed, options).then(function(reply) {
|
||||
this.onExternalUpdate({ typed: typed });
|
||||
return reply;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* A shortcut for calling update, resolving the promise and then exec.
|
||||
* @param input The string to execute
|
||||
|
@ -106,6 +106,8 @@ var commandLanguage = exports.commandLanguage = {
|
||||
var mapping = cli.getMapping(this.requisition.executionContext);
|
||||
mapping.terminal = this.terminal;
|
||||
|
||||
this.requisition.onExternalUpdate.add(this.textChanged, this);
|
||||
|
||||
return this;
|
||||
}.bind(this));
|
||||
},
|
||||
@ -115,6 +117,7 @@ var commandLanguage = exports.commandLanguage = {
|
||||
delete mapping.terminal;
|
||||
|
||||
this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
|
||||
this.requisition.onExternalUpdate.remove(this.textChanged, this);
|
||||
|
||||
this.terminal = undefined;
|
||||
this.requisition = undefined;
|
||||
@ -163,7 +166,14 @@ var commandLanguage = exports.commandLanguage = {
|
||||
// Called internally whenever we think that the current assignment might
|
||||
// have changed, typically on mouse-clicks or key presses.
|
||||
caretMoved: function(start) {
|
||||
if (!this.requisition.isUpToDate()) {
|
||||
return;
|
||||
}
|
||||
var newAssignment = this.requisition.getAssignmentAt(start);
|
||||
if (newAssignment == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.assignment !== newAssignment) {
|
||||
if (this.assignment.param.type.onLeave) {
|
||||
this.assignment.param.type.onLeave(this.assignment);
|
||||
|
@ -87,6 +87,7 @@ function Inputter(options, components) {
|
||||
this.onResize = util.createEvent('Inputter.onResize');
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
this.document.defaultView.addEventListener('resize', this.onWindowResize, false);
|
||||
this.requisition.onExternalUpdate.add(this.textChanged, this);
|
||||
|
||||
this._previousValue = undefined;
|
||||
this.requisition.update(this.element.value || '');
|
||||
@ -99,6 +100,7 @@ Inputter.prototype.destroy = function() {
|
||||
this.document.defaultView.removeEventListener('resize', this.onWindowResize, false);
|
||||
|
||||
this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
|
||||
this.requisition.onExternalUpdate.remove(this.textChanged, this);
|
||||
if (this.focusManager) {
|
||||
this.focusManager.removeMonitoredElement(this.element, 'input');
|
||||
}
|
||||
@ -309,7 +311,13 @@ Inputter.prototype._checkAssignment = function(start) {
|
||||
if (start == null) {
|
||||
start = this.element.selectionStart;
|
||||
}
|
||||
if (!this.requisition.isUpToDate()) {
|
||||
return;
|
||||
}
|
||||
var newAssignment = this.requisition.getAssignmentAt(start);
|
||||
if (newAssignment == null) {
|
||||
return;
|
||||
}
|
||||
if (this.assignment !== newAssignment) {
|
||||
if (this.assignment.param.type.onLeave) {
|
||||
this.assignment.param.type.onLeave(this.assignment);
|
||||
|
@ -276,7 +276,7 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
|
||||
* created, in order to instrument the specified objects and become
|
||||
* aware of everything the content does with them.
|
||||
*/
|
||||
setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak }) {
|
||||
setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
@ -286,6 +286,7 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
|
||||
this._tracedGlobals = tracedGlobals || [];
|
||||
this._tracedFunctions = tracedFunctions || [];
|
||||
this._holdWeak = !!holdWeak;
|
||||
this._storeCalls = !!storeCalls;
|
||||
this._contentObserver = new ContentObserver(this.tabActor);
|
||||
|
||||
on(this._contentObserver, "global-created", this._onGlobalCreated);
|
||||
@ -303,7 +304,8 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
|
||||
tracedFunctions: Option(0, "nullable:array:string"),
|
||||
startRecording: Option(0, "boolean"),
|
||||
performReload: Option(0, "boolean"),
|
||||
holdWeak: Option(0, "boolean")
|
||||
holdWeak: Option(0, "boolean"),
|
||||
storeCalls: Option(0, "boolean")
|
||||
},
|
||||
oneway: true
|
||||
}),
|
||||
@ -541,7 +543,11 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
|
||||
return;
|
||||
}
|
||||
let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);
|
||||
this._functionCalls.push(functionCall);
|
||||
|
||||
if (this._storeCalls) {
|
||||
this._functionCalls.push(functionCall);
|
||||
}
|
||||
|
||||
this.onCall(functionCall);
|
||||
}
|
||||
});
|
||||
@ -552,7 +558,6 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
|
||||
let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
|
||||
initialize: function(client, { callWatcherActor }) {
|
||||
protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -256,7 +256,8 @@ let CanvasActor = exports.CanvasActor = protocol.ActorClass({
|
||||
this._callWatcher.setup({
|
||||
tracedGlobals: CANVAS_CONTEXTS,
|
||||
tracedFunctions: ANIMATION_GENERATORS,
|
||||
performReload: reload
|
||||
performReload: reload,
|
||||
storeCalls: true
|
||||
});
|
||||
}, {
|
||||
request: { reload: Option(0, "boolean") },
|
||||
@ -722,7 +723,6 @@ let ContextUtils = {
|
||||
let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
|
||||
initialize: function(client, { canvasActor }) {
|
||||
protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -197,7 +197,6 @@ let DeviceFront = protocol.FrontClass(DeviceActor, {
|
||||
initialize: function(client, form) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = form.deviceActor;
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
|
||||
|
@ -84,7 +84,6 @@ exports.EventLoopLagFront = protocol.FrontClass(EventLoopLagActor, {
|
||||
initialize: function(client, form) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = form.eventLoopLagActor;
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
});
|
||||
|
@ -202,7 +202,6 @@ exports.GcliFront = protocol.FrontClass(GcliActor, {
|
||||
|
||||
// XXX: This is the first actor type in its hierarchy to use the protocol
|
||||
// library, so we're going to self-own on the client side for now.
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
});
|
||||
|
@ -2724,7 +2724,6 @@ var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor
|
||||
|
||||
// XXX: This is the first actor type in its hierarchy to use the protocol
|
||||
// library, so we're going to self-own on the client side for now.
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
|
||||
|
@ -133,7 +133,6 @@ let ReflowActor = protocol.ActorClass({
|
||||
exports.ReflowFront = protocol.FrontClass(ReflowActor, {
|
||||
initialize: function(client, {reflowActor}) {
|
||||
protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor});
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
|
||||
|
@ -86,7 +86,6 @@ exports.MemoryFront = protocol.FrontClass(MemoryActor, {
|
||||
initialize: function(client, form) {
|
||||
protocol.Front.prototype.initialize.call(this, client, form);
|
||||
this.actorID = form.memoryActor;
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -109,7 +109,6 @@ let PreferenceFront = protocol.FrontClass(PreferenceActor, {
|
||||
initialize: function(client, form) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = form.preferenceActor;
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
});
|
||||
|
@ -1702,8 +1702,6 @@ let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, {
|
||||
initialize: function(client, tabForm) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = tabForm.storageActor;
|
||||
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -254,8 +254,6 @@ let StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
|
||||
initialize: function(client, tabForm) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = tabForm.styleEditorActor;
|
||||
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
|
||||
|
@ -269,8 +269,6 @@ let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
|
||||
initialize: function(client, tabForm) {
|
||||
protocol.Front.prototype.initialize.call(this, client);
|
||||
this.actorID = tabForm.styleSheetsActor;
|
||||
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -270,7 +270,6 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
|
||||
let AudioNodeFront = protocol.FrontClass(AudioNodeActor, {
|
||||
initialize: function (client, form) {
|
||||
protocol.Front.prototype.initialize.call(this, client, form);
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
@ -330,7 +329,8 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||
tracedGlobals: AUDIO_GLOBALS,
|
||||
startRecording: true,
|
||||
performReload: reload,
|
||||
holdWeak: true
|
||||
holdWeak: true,
|
||||
storeCalls: false
|
||||
});
|
||||
// Bind to the `global-destroyed` event on the content observer so we can
|
||||
// unbind events between the global destruction and the `finalize` cleanup
|
||||
@ -580,7 +580,6 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||
let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, {
|
||||
initialize: function(client, { webaudioActor }) {
|
||||
protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor });
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
@ -343,7 +343,6 @@ let WebGLActor = exports.WebGLActor = protocol.ActorClass({
|
||||
let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
|
||||
initialize: function(client, { webglActor }) {
|
||||
protocol.Front.prototype.initialize.call(this, client, { actor: webglActor });
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user