Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-07-03 16:35:21 -04:00
commit 882b7f5864
74 changed files with 2631 additions and 2231 deletions

View File

@ -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 + '")';
},
/**

View File

@ -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);

View File

@ -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);
},
};

View File

@ -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>

View 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);

View File

@ -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
});
},

View File

@ -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);

View File

@ -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>

View File

@ -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 {

View File

@ -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);

View File

@ -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));
}
},

View File

@ -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$

View File

@ -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;
};

View File

@ -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
});
});
}

View File

@ -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)

View File

@ -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>

View File

@ -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);

View File

@ -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();
}

View 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);
}));
});
});
});
});

View File

@ -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>

View File

@ -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);
});
});
});
});

View File

@ -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);
}));
});
});
});
});

View File

@ -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>

View File

@ -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
});
});

View File

@ -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 () {

View File

@ -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);
}));
});
});
});
});

View File

@ -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() {

View File

@ -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

View File

@ -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();"/>

View File

@ -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"/>

View File

@ -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.");

View File

@ -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)

View File

@ -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);
},
/**

View File

@ -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]

View File

@ -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");
}
});

View File

@ -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);
}
});

View File

@ -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]

View 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();
});

View 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: () => {}
};
}

View 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");
}

View 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
};
}

View 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);
}

View File

@ -3,4 +3,6 @@ head =
tail =
firefox-appdir = browser
[test_bezierCanvas.js]
[test_cubicBezier.js]
[test_undoStack.js]

View 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;
}

View 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>

View 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;
}
}

View File

@ -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>

View File

@ -2095,8 +2095,6 @@ public class GeckoAppShell
@WrapElementForJNI(allowMultithread = true)
public static Context getContext() {
if (sContextGetter == null)
return null;
return sContextGetter.getContext();
}

View File

@ -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;

View File

@ -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);

View File

@ -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()) {

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
});

View File

@ -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);
}
});

View File

@ -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);
},

View File

@ -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);
},
});

View File

@ -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);
},
});

View File

@ -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);
},

View File

@ -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);
},

View File

@ -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);
}
});

View File

@ -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);
},
});

View File

@ -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);
}
});

View File

@ -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);
},

View File

@ -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);
}
});

View File

@ -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);
}
});

View File

@ -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);
}
});