Merge m-c to inbound. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-06-09 18:13:50 -04:00
commit 7f089812ff
202 changed files with 44642 additions and 812 deletions

View File

@ -101,3 +101,4 @@ ad0ae007aa9e03cd74e9005cd6652e544139b3b5 FIREFOX_AURORA_25_BASE
ba2cc1eda988a1614d8986ae145d28e1268409b9 FIREFOX_AURORA_29_BASE
83c9853e136451474dfa6d1aaa60a7fca7d2d83a FIREFOX_AURORA_30_BASE
cfde3603b0206e119abea76fdd6e134b634348f1 FIREFOX_AURORA_31_BASE
16f3cac5e8fe471e12f76d6a94a477b14e78df7c FIREFOX_AURORA_32_BASE

View File

@ -463,6 +463,17 @@ pref("services.push.pingInterval", 1800000); // 30 minutes
pref("services.push.requestTimeout", 10000);
// enable udp wakeup support
pref("services.push.udp.wakeupEnabled", true);
// This value should be the prefix to be added to the current PDP context[1]
// domain or a full-qualified domain name.
// If finished with a dot, it will be added as a prefix to the PDP context
// domain. If not, will be used as the DNS query.
// If the DNS query is unsuccessful, the push agent will send a null netid and
// is a server decision what to do with the device. If the MCC-MNC identifies a
// unique network the server will change to UDP mode. Otherwise, a websocket
// connection will be maintained.
// [1] Packet Data Protocol
// http://en.wikipedia.org/wiki/GPRS_core_network#PDP_context
pref("services.push.udp.well-known_netidAddress", "_wakeup_.");
// NetworkStats
#ifdef MOZ_WIDGET_GONK

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>

View File

@ -12,12 +12,12 @@
<!--original fetch url was https://git.mozilla.org/releases-->
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
<!-- B2G specific things. -->
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>

View File

@ -12,12 +12,12 @@
<!--original fetch url was https://git.mozilla.org/releases-->
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
<!-- B2G specific things. -->
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "0d0d9de7b2534dbeb4809ad019294000eb719a80",
"revision": "70cb8b50e7ad2bb64a7ac43e9b9e0c965ae8cd2f",
"repo_path": "/integration/gaia-central"
}

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -12,12 +12,12 @@
<!--original fetch url was https://git.mozilla.org/releases-->
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
<!-- B2G specific things. -->
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -5,12 +5,12 @@
MOZ_APP_BASENAME=B2G
MOZ_APP_VENDOR=Mozilla
MOZ_APP_VERSION=32.0a1
MOZ_APP_VERSION=33.0a1
MOZ_APP_UA_NAME=Firefox
MOZ_UA_OS_AGNOSTIC=1
MOZ_B2G_VERSION=2.0.0.0-prerelease
MOZ_B2G_VERSION=2.1.0.0-prerelease
MOZ_B2G_OS_NAME=Boot2Gecko
MOZ_BRANDING_DIRECTORY=b2g/branding/unofficial

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,68 @@
// 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/.
// the "exported" symbols
let LoopUI;
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
(function() {
LoopUI = {
/**
* Opens the panel for Loop and sizes it appropriately.
*
* @param {event} event The event opening the panel, used to anchor
* the panel to the button which triggers it.
*/
openCallPanel: function(event) {
let panel = document.getElementById("loop-panel");
let anchor = event.target;
let iframe = document.getElementById("loop-panel-frame");
if (!iframe) {
// XXX This should be using SharedFrame (bug 1011392 may do this).
iframe = document.createElement("iframe");
iframe.setAttribute("id", "loop-panel-frame");
iframe.setAttribute("type", "content");
iframe.setAttribute("class", "loop-frame social-panel-frame");
iframe.setAttribute("flex", "1");
panel.appendChild(iframe);
}
// We inject in DOMContentLoaded as that is before any scripts have tun.
iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
injectLoopAPI(iframe.contentWindow);
// We use loopPanelInitialized so that we know we've finished localising before
// sizing the panel.
iframe.contentWindow.addEventListener("loopPanelInitialized",
function documentLoaded() {
iframe.contentWindow.removeEventListener("loopPanelInitialized",
documentLoaded, true);
// XXX We end up with the wrong size here, so this
// needs further investigation (bug 1011394).
sizeSocialPanelToContent(panel, iframe);
}, true);
}, true);
iframe.setAttribute("src", "about:looppanel");
panel.hidden = false;
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
},
/**
* Triggers the initialization of the loop service. Called by
* delayedStartup.
*/
initialize: function() {
MozLoopService.initialize();
},
};
})();

View File

@ -172,6 +172,9 @@ let gInitialPages = [
#include browser-feeds.js
#include browser-fullScreen.js
#include browser-fullZoom.js
#ifdef MOZ_LOOP
#include browser-loop.js
#endif
#include browser-places.js
#include browser-plugins.js
#include browser-safebrowsing.js
@ -1182,6 +1185,10 @@ var gBrowserInit = {
gDataNotificationInfoBar.init();
#endif
#ifdef MOZ_LOOP
LoopUI.initialize();
#endif
gBrowserThumbnails.init();
// Add Devtools menuitems and listeners
@ -5311,8 +5318,8 @@ function setStyleDisabled(disabled) {
var LanguageDetectionListener = {
init: function() {
window.messageManager.addMessageListener("LanguageDetection:Result", msg => {
Translation.languageDetected(msg.target, msg.data);
window.messageManager.addMessageListener("Translation:DocumentState", msg => {
Translation.documentStateReceived(msg.target, msg.data);
});
}
};

View File

@ -226,7 +226,7 @@
<hbox id="UITourTooltipButtons" flex="1" align="center"/>
</vbox>
</panel>
<!-- type="default" forces frames to be created so that the panel's size can be determined -->
<!-- type="default" forces frames to be created so that the panel's size can be determined -->
<panel id="UITourHighlightContainer"
type="default"
hidden="true"
@ -267,6 +267,14 @@
noautofocus="true"
position="topcenter topright"/>
#ifdef MOZ_LOOP
<panel id="loop-panel"
class="loop-panel social-panel"
type="arrow"
orient="horizontal"
hidden="true"/>
#endif
<menupopup id="toolbar-context-menu"
onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
<menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
@ -635,7 +643,11 @@
aria-label="&navbarCmd.label;"
fullscreentoolbar="true" mode="icons" customizable="true"
iconsize="small"
#ifdef MOZ_LOOP
defaultset="urlbar-container,search-container,webrtc-status-button,bookmarks-menu-button,downloads-button,home-button,loop-call-button,social-share-button,social-toolbar-item"
#else
defaultset="urlbar-container,search-container,webrtc-status-button,bookmarks-menu-button,downloads-button,home-button,social-share-button,social-toolbar-item"
#endif
customizationtarget="nav-bar-customization-target"
overflowable="true"
overflowbutton="nav-bar-overflow-button"
@ -768,6 +780,20 @@
oncommand="WebrtcIndicator.menuCommand(event.target);"/>
</toolbarbutton>
#ifdef MOZ_LOOP
<!-- XXX Bug 1013989 will provide a label for the button -->
<toolbarbutton id="loop-call-button"
class="toolbarbutton-1 chromeclass-toolbar-additional"
persist="class"
removable="true"
tooltiptext="&loopCallButton.tooltip;"
oncommand="LoopUI.openCallPanel(event);"
cui-areatype="toolbar"
overflows="false"
>
</toolbarbutton>
#endif
<toolbarbutton id="bookmarks-menu-button"
class="toolbarbutton-1 chromeclass-toolbar-additional"
persist="class"

View File

@ -14,6 +14,7 @@ const kWhitelist = [
{sourceName: /codemirror\.css/i}, /* CodeMirror is imported as-is, see bug 1004423 */
{sourceName: /web\/viewer\.css/i, errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i }, /* PDFjs is futureproofing its pseudoselectors, and those rules are dropped. */
{sourceName: /aboutaccounts\/(main|normalize)\.css/i}, /* Tracked in bug 1004428 */
{sourceName: /otcdn\/webrtc\/.*\.css$/i /* TokBox SDK assets, see bug 1003029 */}
];
/**

View File

@ -97,6 +97,16 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::ALLOW_SCRIPT },
{ "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xul",
nsIAboutModule::ALLOW_SCRIPT },
#ifdef MOZ_LOOP
{ "loopconversation", "chrome://browser/content/loop/conversation.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{ "looppanel", "chrome://browser/content/loop/panel.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
#endif
};
static const int kRedirTotal = ArrayLength(kRedirMap);
@ -119,7 +129,7 @@ GetAboutModuleName(nsIURI *aURI)
}
NS_IMETHODIMP
AboutRedirector::NewChannel(nsIURI *aURI, nsIChannel **result)
AboutRedirector::NewChannel(nsIURI *aURI, nsIChannel **result)
{
NS_ENSURE_ARG_POINTER(aURI);
NS_ASSERTION(result, "must not be null");

View File

@ -112,6 +112,10 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
#endif
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "app-manager", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#ifdef MOZ_LOOP
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#endif
#if defined(XP_WIN)
{ NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
#elif defined(XP_MACOSX)

View File

@ -200,6 +200,9 @@ let CustomizableUIInternal = {
"bookmarks-menu-button",
"downloads-button",
"home-button",
#ifdef MOZ_LOOP
"loop-call-button",
#endif
"social-share-button",
],
defaultCollapsed: false,

View File

@ -0,0 +1,170 @@
/* 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";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
/**
* Inject the loop API into the given window. The caller must be sure the
* window is a loop content window (eg, a panel, chatwindow, or similar).
*
* See the documentation on the individual functions for details of the API.
*
* @param {nsIDOMWindow} targetWindow The content window to attach the API.
*/
function injectLoopAPI(targetWindow) {
let api = {
/**
* Sets and gets the "do not disturb" mode activation flag.
*/
doNotDisturb: {
enumerable: true,
configurable: true,
get: function() {
return MozLoopService.doNotDisturb;
},
set: function(aFlag) {
MozLoopService.doNotDisturb = aFlag;
}
},
/**
* 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.
*
* @returns {String} The locale string
*/
locale: {
enumerable: true,
configurable: true,
get: function() {
return MozLoopService.locale;
}
},
/**
* Returns translated strings associated with an element. Designed
* for use with l10n.js
*
* @param {String} key The element id
* @returns {Object} A JSON string containing the localized
* attribute/value pairs for the element.
*/
getStrings: {
enumerable: true,
configurable: true,
writable: true,
value: function(key) {
return MozLoopService.getStrings(key);
}
},
/**
* Call to ensure that any necessary registrations for the Loop Service
* have taken place.
*
* Callback parameters:
* - err null on successful registration, non-null otherwise.
*
* @param {Function} callback Will be called once registration is complete,
* or straight away if registration has already
* happened.
*/
ensureRegistered: {
enumerable: true,
configurable: true,
writable: true,
value: function(callback) {
// We translate from a promise to a callback, as we can't pass promises from
// Promise.jsm across the priv versus unpriv boundary.
return MozLoopService.register().then(() => {
callback(null);
}, err => {
callback(err);
});
}
},
/**
* Used to note a call url expiry time. If the time is later than the current
* latest expiry time, then the stored expiry time is increased. For times
* sooner, this function is a no-op; this ensures we always have the latest
* expiry time for a url.
*
* This is used to determine whether or not we should be registering with the
* push server on start.
*
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
* of the url.
*/
noteCallUrlExpiry: {
enumerable: true,
configurable: true,
writable: true,
value: function(expiryTimeSeconds) {
MozLoopService.noteCallUrlExpiry(expiryTimeSeconds);
}
},
/**
* Return any preference under "loop." that's coercible to a character
* preference.
*
* @param {String} prefName The name of the pref without the preceding
* "loop."
*
* Any errors thrown by the Mozilla pref API are logged to the console
* and cause null to be returned. This includes the case of the preference
* not being found.
*
* @return {String} on success, null on error
*/
getLoopCharPref: {
enumerable: true,
configurable: true,
writable: true,
value: function(prefName) {
return MozLoopService.getLoopCharPref(prefName);
}
}
};
let contentObj = Cu.createObjectIn(targetWindow);
Object.defineProperties(contentObj, api);
Cu.makeObjectPropsNormal(contentObj);
targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
// We do this in a getter, so that we create these objects
// only on demand (this is a potential concern, since
// otherwise we might add one per iframe, and keep them
// alive for as long as the window is alive).
delete targetWindow.navigator.wrappedJSObject.mozLoop;
return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
});
// Handle window.close correctly on the panel and chatbox.
hookWindowCloseForPanelClose(targetWindow);
}

View File

@ -0,0 +1,593 @@
/* 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";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
this.EXPORTED_SYMBOLS = ["MozLoopService"];
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
"resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
"resource://services-crypto/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "HAWKAuthenticatedRESTRequest",
"resource://services-common/hawkrequest.js");
/**
* We don't have push notifications on desktop currently, so this is a
* workaround to get them going for us.
*
* XXX Handle auto-reconnections if connection fails for whatever reason
* (bug 1013248).
*/
let PushHandlerHack = {
// This is the uri of the push server.
pushServerUri: Services.prefs.getCharPref("services.push.serverURL"),
// This is the channel id we're using for notifications
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
// Stores the push url if we're registered and we have one.
pushUrl: undefined,
/**
* Call to start the connection to the push socket server. On
* connection, it will automatically say hello and register the channel
* id with the server.
*
* Register callback parameters:
* - {String|null} err: Encountered error, if any
* - {String} url: The push url obtained from the server
*
* @param {Function} registerCallback Callback to be called once we are
* registered.
* @param {Function} notificationCallback Callback to be called when a
* push notification is received.
*/
initialize: function(registerCallback, notificationCallback) {
if (Services.io.offline) {
registerCallback("offline");
return;
}
this._registerCallback = registerCallback;
this._notificationCallback = notificationCallback;
this.websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
.createInstance(Ci.nsIWebSocketChannel);
this.websocket.protocol = "push-notification";
var pushURI = Services.io.newURI(this.pushServerUri, null, null);
this.websocket.asyncOpen(pushURI, this.pushServerUri, this, null);
},
/**
* Listener method, handles the start of the websocket stream.
* Sends a hello message to the server.
*
* @param {nsISupports} aContext Not used
*/
onStart: function() {
var helloMsg = { messageType: "hello", uaid: "", channelIDs: [] };
this.websocket.sendMsg(JSON.stringify(helloMsg));
},
/**
* Listener method, called when the websocket is closed.
*
* @param {nsISupports} aContext Not used
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
*/
onStop: function(aContext, aStatusCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket is closed by the server.
* If there are errors, onStop may be called without ever calling this
* method.
*
* @param {nsISupports} aContext Not used
* @param {integer} aCode the websocket closing handshake close code
* @param {String} aReason the websocket closing handshake close reason
*/
onServerClose: function(aContext, aCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket receives a message.
*
* @param {nsISupports} aContext Not used
* @param {String} aMsg The message data
*/
onMessageAvailable: function(aContext, aMsg) {
var msg = JSON.parse(aMsg);
switch(msg.messageType) {
case "hello":
this._registerChannel();
break;
case "register":
this.pushUrl = msg.pushEndpoint;
this._registerCallback(null, this.pushUrl);
break;
case "notification":
msg.updates.forEach(function(update) {
if (update.channelID === this.channelID) {
this._notificationCallback(update.version);
}
}.bind(this));
break;
}
},
/**
* Handles registering a service
*/
_registerChannel: function() {
this.websocket.sendMsg(JSON.stringify({
messageType: "register",
channelID: this.channelID
}));
}
};
/**
* Internal helper methods and state
*
* The registration is a two-part process. First we need to connect to
* and register with the push server. Then we need to take the result of that
* and register with the Loop server.
*/
let MozLoopServiceInternal = {
// The uri of the Loop server.
loopServerUri: Services.prefs.getCharPref("loop.server"),
// The current deferred for the registration process. This is set if in progress
// or the registration was successful. This is null if a registration attempt was
// unsuccessful.
_registeredDeferred: null,
/**
* The initial delay for push registration. This ensures we don't start
* kicking off straight after browser startup, just a few seconds later.
*/
get initialRegistrationDelayMilliseconds() {
try {
// Let a pref override this for developer & testing use.
return Services.prefs.getIntPref("loop.initialDelay");
} catch (x) {
// Default to 5 seconds
return 5000;
}
return initialDelay;
},
/**
* Gets the current latest expiry time for urls.
*
* In seconds since epoch.
*/
get expiryTimeSeconds() {
try {
return Services.prefs.getIntPref("loop.urlsExpiryTimeSeconds");
} catch (x) {
// It is ok for the pref not to exist.
return 0;
}
},
/**
* Sets the expiry time to either the specified time, or keeps it the same
* depending on which is latest.
*/
set expiryTimeSeconds(time) {
if (time > this.expiryTimeSeconds) {
Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", time);
}
},
/**
* Returns true if the expiry time is in the future.
*/
urlExpiryTimeIsInFuture: function() {
return this.expiryTimeSeconds * 1000 > Date.now();
},
/**
* Retrieves MozLoopService "do not disturb" pref value.
*
* @return {Boolean} aFlag
*/
get doNotDisturb() {
return Services.prefs.getBoolPref("loop.do_not_disturb");
},
/**
* Sets MozLoopService "do not disturb" pref value.
*
* @param {Boolean} aFlag
*/
set doNotDisturb(aFlag) {
Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
},
/**
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
promiseRegisteredWithServers: function() {
if (this._registeredDeferred) {
return this._registeredDeferred.promise;
}
this._registeredDeferred = Promise.defer();
// We grab the promise early in case .initialize or its results sets
// it back to null on error.
let result = this._registeredDeferred.promise;
PushHandlerHack.initialize(this.onPushRegistered.bind(this),
this.onHandleNotification.bind(this));
return result;
},
/**
* Derives hawk credentials for the given token and context.
*
* @param {String} tokenHex The token value in hex.
* @param {String} context The context for the token.
*/
deriveHawkCredentials: function(tokenHex, context) {
const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
let token = CommonUtils.hexToBytes(tokenHex);
let keyWord = CommonUtils.stringToBytes(PREFIX_NAME + context);
// XXX Using 2 * 32 for now to be in sync with client.js, but we might
// want to make this 3 * 32 to allow for extra, if we start using the extra
// field.
let out = CryptoUtils.hkdf(token, undefined, keyWord, 2 * 32);
return {
algorithm: "sha256",
key: out.slice(32, 64),
id: CommonUtils.bytesAsHex(out.slice(0, 32))
};
},
/**
* Callback from PushHandlerHack - The push server has been registered
* and has given us a push url.
*
* @param {String} pushUrl The push url given by the push server.
*/
onPushRegistered: function(err, pushUrl) {
if (err) {
this._registeredDeferred.reject(err);
this._registeredDeferred = null;
return;
}
this.registerWithLoopServer(pushUrl);
},
/**
* Registers with the Loop server.
*
* @param {String} pushUrl The push url given by the push server.
* @param {Boolean} noRetry Optional, don't retry if authentication fails.
*/
registerWithLoopServer: function(pushUrl, noRetry) {
let sessionToken;
try {
sessionToken = Services.prefs.getCharPref("loop.hawk-session-token");
} catch (x) {
// It is ok for this not to exist, we'll default to sending no-creds
}
let credentials;
if (sessionToken) {
credentials = this.deriveHawkCredentials(sessionToken, "sessionToken");
}
let uri = Services.io.newURI(this.loopServerUri, null, null).resolve("/registration");
this.loopXhr = new HAWKAuthenticatedRESTRequest(uri, credentials);
this.loopXhr.dispatch('POST', { simple_push_url: pushUrl }, (error) => {
if (this.loopXhr.response.status == 401) {
if (this.urlExpiryTimeIsInFuture()) {
// XXX Should this be reported to the user is a visible manner?
Cu.reportError("Loop session token is invalid, all previously "
+ "generated urls will no longer work.");
}
// Authorization failed, invalid token, we need to try again with a new token.
Services.prefs.clearUserPref("loop.hawk-session-token");
this.registerWithLoopServer(pushUrl, true);
return;
}
// No authorization issues, so complete registration.
this.onLoopRegistered(error);
});
},
/**
* Callback from PushHandlerHack - A push notification has been received from
* the server.
*
* @param {String} version The version information from the server.
*/
onHandleNotification: function(version) {
if (this.doNotDisturb) {
return;
}
this.openChatWindow(null, "LooP", "about:loopconversation#incoming/" + version);
},
/**
* Callback from the loopXhr. Checks the registration result.
*/
onLoopRegistered: function(error) {
let status = this.loopXhr.response.status;
if (status != 200) {
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
Cu.reportError("Failed to register with the loop server. Code: " +
status + " Text: " + this.loopXhr.response.statusText);
this._registeredDeferred.reject(status);
this._registeredDeferred = null;
return;
}
let sessionToken = this.loopXhr.response.headers["hawk-session-token"];
if (sessionToken) {
// XXX should do more validation here
if (sessionToken.length === 64) {
Services.prefs.setCharPref("loop.hawk-session-token", sessionToken);
} else {
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
console.warn("Loop server sent an invalid session token");
this._registeredDeferred.reject("session-token-wrong-size");
this._registeredDeferred = null;
return;
}
}
// If we made it this far, we registered just fine.
this.registeredLoopServer = true;
this._registeredDeferred.resolve();
// No need to clear the promise here, everything was good, so we don't need
// to re-register.
},
/**
* A getter to obtain and store the strings for loop. This is structured
* for use by l10n.js.
*
* @returns {Object} a map of element ids with attributes to set.
*/
get localizedStrings() {
if (this._localizedStrings)
return this._localizedStrings;
var stringBundle =
Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
var map = {};
var enumerator = stringBundle.getSimpleEnumeration();
while (enumerator.hasMoreElements()) {
var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
// 'textContent' is the default attribute to set if none are specified.
var key = string.key, property = 'textContent';
var i = key.lastIndexOf('.');
if (i >= 0) {
property = key.substring(i + 1);
key = key.substring(0, i);
}
if (!(key in map))
map[key] = {};
map[key][property] = string.value;
}
return this._localizedStrings = map;
},
/**
* Opens the chat window
*
* @param {Object} contentWindow The window to open the chat window in, may
* be null.
* @param {String} title The title of the chat window.
* @param {String} url The page to load in the chat window.
* @param {String} mode May be "minimized" or undefined.
*/
openChatWindow: function(contentWindow, title, url, mode) {
// So I guess the origin is the loop server!?
let origin = this.loopServerUri;
url = url.spec || url;
let callback = chatbox => {
// We need to use DOMContentLoaded as otherwise the injection will happen
// in about:blank and then get lost.
// Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
// involves event loop spins, which means it might be too late.
// Have we already done it?
if (chatbox.contentWindow.navigator.mozLoop) {
return;
}
chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
if (event.target != chatbox.contentDocument) {
return;
}
chatbox.removeEventListener("DOMContentLoaded", loaded, true);
injectLoopAPI(chatbox.contentWindow);
}, true);
};
Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
}
};
/**
* Public API
*/
this.MozLoopService = {
/**
* Initialized the loop service, and starts registration with the
* push and loop servers.
*/
initialize: function() {
// If expiresTime is in the future then kick-off registration.
if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
this._startInitializeTimer();
}
},
/**
* Internal function, exposed for testing purposes only. Used to start the
* initialize timer.
*/
_startInitializeTimer: function() {
// Kick off the push notification service into registering after a timeout
// this ensures we're not doing too much straight after the browser's finished
// starting up.
this._initializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._initializeTimer.initWithCallback(function() {
this.register();
this._initializeTimer = null;
}.bind(this),
MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
},
/**
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
register: function() {
return MozLoopServiceInternal.promiseRegisteredWithServers();
},
/**
* Used to note a call url expiry time. If the time is later than the current
* latest expiry time, then the stored expiry time is increased. For times
* sooner, this function is a no-op; this ensures we always have the latest
* expiry time for a url.
*
* This is used to deterimine whether or not we should be registering with the
* push server on start.
*
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
* of the url.
*/
noteCallUrlExpiry: function(expiryTimeSeconds) {
MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
},
/**
* Returns the strings for the specified element. Designed for use
* with l10n.js.
*
* @param {key} The element id to get strings for.
* @return {String} A JSON string containing the localized
* attribute/value pairs for the element.
*/
getStrings: function(key) {
var stringData = MozLoopServiceInternal.localizedStrings;
if (!(key in stringData)) {
Cu.reportError('No string for key: ' + key + 'found');
return "";
}
return JSON.stringify(stringData[key]);
},
/**
* Retrieves MozLoopService "do not disturb" value.
*
* @return {Boolean}
*/
get doNotDisturb() {
return MozLoopServiceInternal.doNotDisturb;
},
/**
* Sets MozLoopService "do not disturb" value.
*
* @param {Boolean} aFlag
*/
set doNotDisturb(aFlag) {
MozLoopServiceInternal.doNotDisturb = aFlag;
},
/**
* Returns the current locale
*
* @return {String} The code of the current locale.
*/
get locale() {
try {
return Services.prefs.getComplexValue("general.useragent.locale",
Ci.nsISupportsString).data;
} catch (ex) {
return "en-US";
}
},
/**
* Return any preference under "loop." that's coercible to a character
* preference.
*
* @param {String} prefName The name of the pref without the preceding
* "loop."
*
* Any errors thrown by the Mozilla pref API are logged to the console
* and cause null to be returned. This includes the case of the preference
* not being found.
*
* @return {String} on success, null on error
*/
getLoopCharPref: function(prefName) {
try {
return Services.prefs.getCharPref("loop." + prefName);
} catch (ex) {
console.log("getLoopCharPref had trouble getting " + prefName +
"; exception: " + ex);
return null;
}
}
};

View File

@ -0,0 +1,8 @@
This is the directory for the Loop desktop implementation and the standalone client.
The desktop implementation is the UX built into Firefox, activated by the Loop button on the toolbar. The standalone client is the link-clicker UX for any modern browser that supports WebRTC.
The standalone client is a set of web pages intended to be hosted on a standalone server referenced by the loop-server.
The standalone client exists in standalone/ but shares items (from content/shared/) with the desktop implementation. See the README.md file in the standalone/ directory for how to run the server locally.

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Conversation</title>
<link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
<link rel="stylesheet" type="text/css" href="loop/shared/css/conversation.css">
</head>
<body onload="loop.conversation.init();">
<div id="messages"></div>
<div id="main"></div>
<script type="text/javascript" src="loop/libs/l10n.js"></script>
<script type="text/javascript" src="loop/shared/libs/sdk.js"></script>
<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/desktopRouter.js"></script>
<script type="text/javascript" src="loop/js/conversation.js"></script>
</body>
</html>

View File

@ -0,0 +1,190 @@
/* 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 */
var loop = loop || {};
loop.conversation = (function(OT, mozL10n) {
"use strict";
var sharedViews = loop.shared.views,
// aliasing translation function as __ for concision
__ = mozL10n.get;
/**
* App router.
* @type {loop.desktopRouter.DesktopConversationRouter}
*/
var router;
/**
* Incoming call view.
* @type {loop.shared.views.BaseView}
*/
var IncomingCallView = sharedViews.BaseView.extend({
template: _.template([
'<h2 data-l10n-id="incoming_call"></h2>',
'<p>',
' <button class="btn btn-success btn-accept"',
' data-l10n-id="accept_button"></button>',
' <button class="btn btn-error btn-decline"',
' data-l10n-id="decline_button"></button>',
'</p>'
].join("")),
className: "incoming-call",
events: {
"click .btn-accept": "handleAccept",
"click .btn-decline": "handleDecline"
},
/**
* User clicked on the "accept" button.
* @param {MouseEvent} event
*/
handleAccept: function(event) {
event.preventDefault();
this.model.trigger("accept");
},
/**
* User clicked on the "decline" button.
* @param {MouseEvent} event
*/
handleDecline: function(event) {
event.preventDefault();
// XXX For now, we just close the window.
window.close();
}
});
/**
* Call ended view.
* @type {loop.shared.views.BaseView}
*/
var EndedCallView = sharedViews.BaseView.extend({
template: _.template([
'<p>',
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
'</p>'
].join("")),
className: "call-ended",
events: {
"click button": "closeWindow"
},
closeWindow: function(event) {
event.preventDefault();
// XXX For now, we just close the window.
window.close();
}
});
/**
* Conversation router.
*
* Required options:
* - {loop.shared.models.ConversationModel} conversation Conversation model.
* - {loop.shared.components.Notifier} notifier Notifier component.
*
* @type {loop.shared.router.BaseConversationRouter}
*/
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
routes: {
"incoming/:version": "incoming",
"call/accept": "accept",
"call/ongoing": "conversation",
"call/ended": "ended"
},
/**
* @override {loop.shared.router.BaseConversationRouter.startCall}
*/
startCall: function() {
this.navigate("call/ongoing", {trigger: true});
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/
endCall: function() {
this.navigate("call/ended", {trigger: true});
},
/**
* Incoming call route.
*
* @param {String} loopVersion The version from the push notification, set
* by the router from the URL.
*/
incoming: function(loopVersion) {
this._conversation.set({loopVersion: loopVersion});
this._conversation.once("accept", function() {
this.navigate("call/accept", {trigger: true});
}.bind(this));
this.loadView(new IncomingCallView({model: this._conversation}));
},
/**
* Accepts an incoming call.
*/
accept: function() {
this._conversation.initiate({
baseServerUrl: window.navigator.mozLoop.serverUrl,
outgoing: false
});
},
/**
* conversation is the route when the conversation is active. The start
* route should be navigated to first.
*/
conversation: function() {
if (!this._conversation.isSessionReady()) {
console.error("Error: navigated to conversation route without " +
"the start route to initialise the call first");
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}
this.loadView(
new loop.shared.views.ConversationView({
sdk: OT,
model: this._conversation
}));
},
/**
* XXX: load a view with a close button for now?
*/
ended: function() {
this.loadView(new EndedCallView());
}
});
/**
* Panel initialisation.
*/
function init() {
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(window.navigator.mozLoop);
router = new ConversationRouter({
conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
notifier: new sharedViews.NotificationListView({el: "#messages"})
});
Backbone.history.start();
}
return {
ConversationRouter: ConversationRouter,
EndedCallView: EndedCallView,
IncomingCallView: IncomingCallView,
init: init
};
})(window.OT, document.mozL10n);

View File

@ -0,0 +1,34 @@
/* 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 */
var loop = loop || {};
loop.desktopRouter = (function() {
"use strict";
/**
* On the desktop app, the use of about: uris prevents us from changing the
* url of the location. As a result, we change the navigate function to simply
* activate the new routes, and not try changing the url.
*
* XXX It is conceivable we might be able to remove this in future, if we
* can either swap to resource uris or remove the limitation on the about uris.
*/
var extendedRouter = {
navigate: function(to) {
this[this.routes[to]]();
}
};
var DesktopRouter = loop.shared.router.BaseRouter.extend(extendedRouter);
var DesktopConversationRouter =
loop.shared.router.BaseConversationRouter.extend(extendedRouter);
return {
DesktopRouter: DesktopRouter,
DesktopConversationRouter: DesktopConversationRouter
};
})();

View File

@ -0,0 +1,248 @@
/* 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 */
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views,
// aliasing translation function as __ for concision
__ = mozL10n.get;
/**
* Panel router.
* @type {loop.desktopRouter.DesktopRouter}
*/
var router;
/**
* Do not disturb panel subview.
*/
var DoNotDisturbView = sharedViews.BaseView.extend({
template: _.template([
'<label>',
' <input type="checkbox" <%- checked %>>',
' <span data-l10n-id="do_not_disturb"></span>',
'</label>',
].join('')),
events: {
"click input[type=checkbox]": "toggle"
},
/**
* Toggles mozLoop activation status.
*/
toggle: function() {
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
this.render();
},
render: function() {
this.$el.html(this.template({
checked: navigator.mozLoop.doNotDisturb ? "checked" : ""
}));
return this;
}
});
/**
* Panel view.
*/
var PanelView = sharedViews.BaseView.extend({
template: _.template([
'<div class="description">',
' <p data-l10n-id="get_link_to_share"></p>',
'</div>',
'<div class="action">',
' <form class="invite">',
' <input type="text" name="caller" data-l10n-id="caller" required>',
' <button type="submit" class="get-url btn btn-success"',
' data-l10n-id="get_a_call_url"></button>',
' </form>',
' <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>',
' </p>',
' <p class="dnd"></p>',
'</div>',
].join("")),
className: "share generate-url",
/**
* Do not disturb view.
* @type {DoNotDisturbView|undefined}
*/
dndView: undefined,
events: {
"keyup input[name=caller]": "changeButtonState",
"submit form.invite": "getCallUrl",
"click a.go-back": "goBack"
},
initialize: function(options) {
options = options || {};
if (!options.notifier) {
throw new Error("missing required notifier");
}
this.notifier = options.notifier;
this.client = new loop.shared.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
},
getNickname: function() {
return this.$("input[name=caller]").val();
},
getCallUrl: function(event) {
this.notifier.clear();
event.preventDefault();
var callback = function(err, callUrlData) {
this.clearPending();
if (err) {
this.notifier.errorL10n("unable_retrieve_url");
this.render();
return;
}
this.onCallUrlReceived(callUrlData);
}.bind(this);
this.setPending();
this.client.requestCallUrl(this.getNickname(), callback);
},
goBack: function(event) {
event.preventDefault();
this.$(".action .result").hide();
this.$(".action .invite").show();
this.$(".description p").text(__("get_link_to_share"));
this.changeButtonState();
},
onCallUrlReceived: function(callUrlData) {
this.notifier.clear();
this.$(".action .invite").hide();
this.$(".action .invite input").val("");
this.$(".action .result input").val(callUrlData.call_url);
this.$(".action .result").show();
this.$(".description p").text(__("share_link_url"));
},
setPending: function() {
this.$("[name=caller]").addClass("pending");
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
},
clearPending: function() {
this.$("[name=caller]").removeClass("pending");
this.changeButtonState();
},
changeButtonState: function() {
var enabled = !!this.$("input[name=caller]").val();
if (enabled) {
this.$(".get-url").removeClass("disabled")
.removeAttr("disabled", "disabled");
} else {
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
}
},
render: function() {
this.$el.html(this.template());
// Do not Disturb sub view
this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
return this;
}
});
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
/**
* DOM document object.
* @type {HTMLDocument}
*/
document: undefined,
routes: {
"": "home"
},
initialize: function(options) {
options = options || {};
if (!options.document) {
throw new Error("missing required document");
}
this.document = options.document;
this._registerVisibilityChangeEvent();
this.on("panel:open panel:closed", this.reset, this);
},
/**
* Register the DOM visibility API event for the whole document, and trigger
* appropriate events accordingly:
*
* - `panel:opened` when the panel is open
* - `panel:closed` when the panel is closed
*
* @link http://www.w3.org/TR/page-visibility/
*/
_registerVisibilityChangeEvent: function() {
this.document.addEventListener("visibilitychange", function(event) {
this.trigger(event.currentTarget.hidden ? "panel:closed"
: "panel:open");
}.bind(this));
},
/**
* Default entry point.
*/
home: function() {
this.reset();
},
/**
* Resets this router to its initial state.
*/
reset: function() {
// purge pending notifications
this._notifier.clear();
// reset home view
this.loadView(new PanelView({notifier: this._notifier}));
}
});
/**
* Panel initialisation.
*/
function init() {
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
router = new PanelRouter({
document: document,
notifier: new sharedViews.NotificationListView({el: "#messages"})
});
Backbone.history.start();
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');
evtObject.initEvent('loopPanelInitialized', true, false);
window.dispatchEvent(evtObject);
}
return {
init: init,
PanelView: PanelView,
DoNotDisturbView: DoNotDisturbView,
PanelRouter: PanelRouter
};
})(_, document.mozL10n);

View File

@ -0,0 +1,119 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
// This is a modified version of l10n.js that the pdf.js extension uses.
// It uses an explicitly passed object for the strings/locale functionality,
// and does not automatically translate on DOMContentLoaded, but requires
// initialize to be called. This improves testability and helps to avoid race
// conditions.
(function(window) {
var gL10nDetails;
var gLanguage = '';
// fetch an l10n objects
function getL10nData(key) {
var response = gL10nDetails.getStrings(key);
var data = JSON.parse(response);
if (!data)
console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']');
return data;
}
// replace {{arguments}} with their values
function substArguments(text, args) {
if (!args)
return text;
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, function(all, name) {
return name in args ? args[name] : '{{' + name + '}}';
});
}
// translate a string
function translateString(key, args, fallback) {
var data = getL10nData(key);
if (!data && fallback)
data = {textContent: fallback};
if (!data)
return '{{' + key + '}}';
return substArguments(data.textContent, args);
}
// translate an HTML element
function translateElement(element) {
if (!element || !element.dataset)
return;
// get the related l10n object
var key = element.dataset.l10nId;
var data = getL10nData(key);
if (!data)
return;
// get arguments (if any)
// TODO: more flexible parser?
var args;
if (element.dataset.l10nArgs) try {
args = JSON.parse(element.dataset.l10nArgs);
} catch (e) {
console.warn('[l10n] could not parse arguments for #' + key + '');
}
// translate element
// TODO: security check?
for (var k in data)
element[k] = substArguments(data[k], args);
}
// translate an HTML subtree
function translateFragment(element) {
element = element || document.querySelector('html');
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = element.querySelectorAll('*[data-l10n-id]');
var elementCount = children.length;
for (var i = 0; i < elementCount; i++)
translateElement(children[i]);
// translate element itself if necessary
if (element.dataset.l10nId)
translateElement(element);
}
// Public API
document.mozL10n = {
/**
* Called to do the initial translation, this should be called
* when DOMContentLoaded is fired, or the equivalent time.
*
* @param {Object} l10nDetails An object implementing the locale attribute
* and getStrings(key) function.
*/
initialize: function(l10nDetails) {
gL10nDetails = l10nDetails;
gLanguage = gL10nDetails.locale;
translateFragment();
},
// get a localized string
get: translateString,
// get the document language
getLanguage: function() { return gLanguage; },
// get the direction (ltr|rtl) of the current language
getDirection: function() {
// http://www.w3.org/International/questions/qa-scripts
// Arabic, Hebrew, Farsi, Pashto, Urdu
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
},
// translate an element or document fragment
translate: translateFragment
};
})(this);

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop Panel</title>
<link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
<link rel="stylesheet" type="text/css" href="loop/shared/css/panel.css">
</head>
<body class="panel" onload="loop.panel.init();">
<div id="messages"></div>
<div id="main"></div>
<script type="text/javascript" src="loop/libs/l10n.js"></script>
<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/desktopRouter.js"></script>
<script type="text/javascript" src="loop/js/panel.js"></script>
</body>
</html>

View File

@ -0,0 +1,12 @@
Loop Shared Web Assets
======================
This directory contains web assets shared across the Loop client webapp and the
Loop Firefox Component.
Warning
-------
Any modification in these files will have possible side effects on both the
Firefox component and the webapp. The `css/readme.html` file uses all the shared
styles, you should use it as a way of checking for visual regressions.

View File

@ -0,0 +1,148 @@
/* 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/. */
/* Generic rules */
/**
* "Fixes" the Box Model.
* @see http://www.paulirish.com/2012/box-sizing-border-box-ftw/
*/
*, *:before, *:after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans;
font-size: 14px;
background: #f2f2f2;
}
button {
font-size: .9em; /* for some reason, text is larger within <button> */
}
img {
border: none;
}
/* Helpers */
/**
* Clearfix impl. for modern browsers
* 1. The space content is one way to avoid an Opera bug when the
* contenteditable attribute is included anywhere else in the document.
* Otherwise it causes space to appear at the top and bottom of elements
* that are clearfixed.
* 2. The use of `table` rather than `block` is only necessary if using
* `:before` to contain the top-margins of child elements.
*/
.cf:before,
.cf:after {
content: " "; /* 1 */
display: table; /* 2 */
}
.cf:after {
clear: both;
}
.hide {
display: none;
}
.tc {
text-align: center;
}
/* Buttons */
.btn {
display: inline-block;
background: #a5a;
border: none;
color: #fff;
text-decoration: none;
padding: .25em .5em .3em;
border-radius: .2em;
}
.btn-info {
background: #428BCA;
}
.btn-success {
background: #5cb85c;
}
.btn-warning {
background: #f0ad4e;
}
.btn-error {
background: #d9534f;
}
.disabled, button[disabled] {
cursor: not-allowed;
pointer-events: none;
opacity: 0.65;
}
/* Alerts */
.alert {
background: #eee;
padding: .2em 1em;
margin-bottom: 1em;
}
.alert p.message {
padding: 0;
margin: 0;
}
.alert.alert-error {
background: #f99;
border: 1px solid #f77;
}
.alert.alert-warning {
background: #fcf8e3;
border: 1px solid #fbeed5;
}
.alert .close {
position: relative;
top: -.2em;
right: -1em;
}
/* Misc */
.close {
float: right;
font-size: 20px;
font-weight: bold;
line-height: 1em;
color: #000;
opacity: .2;
}
.close:before {
/* \2716 is unicode representation of the close button icon */
content: '\2716';
}
button.close {
background: none;
border: none;
cursor: pointer;
}
/* Transitions */
.fade-out {
transition: opacity 0.5s ease-in;
opacity: 0;
}

View File

@ -0,0 +1,73 @@
/* 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/. */
/* Conversation window styles */
.conversation .controls {
background: #f2f2f2;
padding: .5em;
}
.conversation .media video {
background: #eee;
}
/* Nested video elements */
.conversation .media.nested {
position: relative;
}
.conversation .media.nested .remote {
width: 100%;
}
.conversation .media.nested .local {
position: absolute;
bottom: .8em;
right: .8em;
width: 30%;
max-width: 140px;
}
/* Side by side video elements */
.conversation .media.side-by-side .remote {
width: 50%;
float: left;
}
.conversation .media.side-by-side .local {
width: 50%;
}
/**
* Overriden SDK styles; .OT_video-container is using absolute positioning
* therefore moves the video elements it contains outside of the current
* viewport.
*/
.conversation .OT_video-container {
position: inherit;
}
/* Call ended view */
.call-ended p {
text-align: center;
}
/* Incoming call */
.incoming-call {
text-align: center;
min-height: 200px;
}
.incoming-call h2 {
font-size: 1.5em;
font-weight: normal;
margin-top: 3em;
}
.incoming-call button {
margin-right: .2em;
}

View File

@ -0,0 +1,83 @@
/* 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/. */
/* Panel styles */
.panel {
/* XXX the Social API panel behaves weirdly on inner element size changes,
adding unwanted scrollbars; quickfix is to hide these for now. */
overflow: hidden;
}
.spacer {
margin-bottom: 1em;
}
.share {
background: #f2f2f2;
}
.share .description {
background: #f7f7f7 url("../img/icon_32.png") no-repeat 1em 1.5em;
border-bottom: 1px solid #c3c3c3;
padding: 1em 1em 0 4em;
}
.share .description .field {
padding-bottom: 1em;
border-bottom: 1px dotted #ddd;
}
.share .description select {
float: right;
}
.share .description .preview video {
background: #ccc;
float: right;
width: 180px;
}
.share .action {
clear: right;
padding: 1em;
border-top: 1px solid #fafafa;
}
.share .action p {
margin: 0 0 1em 0;
}
.share .action p.dnd {
margin-top: 1em;
}
.share .action input[type="text"],
.share .action input[type="url"] {
border: 1px solid #ccc; /* Overriding background style for a text input (see
below) resets its borders to a weird beveled style;
defining a default 1px border solves the issue. */
font-size: .9em;
width: 65%;
padding: .5em;
}
.share .action input.pending {
background-image: url(../img/loading-icon.gif);
background-repeat: no-repeat;
background-position: right;
}
/* For some reason, buttons have a bigger default font size in FF; we're
reducing a bit for graphical consistency here. */
.share .action button {
font-size: .9em;
padding-top: 6px;
}
/* Specific cases */
.panel #messages .alert {
margin-bottom: 0;
}

View File

@ -0,0 +1,187 @@
<!DOCTYPE html>
<!-- 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/. -->
<!-- This file is intended to help frontend developers to easily identify what
are the available styles for the Loop UI components. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop UI shared CSS information/demo</title>
<link type="text/css" rel="stylesheet" href="common.css">
<link type="text/css" rel="stylesheet" href="panel.css">
<link type="text/css" rel="stylesheet" href="conversation.css">
<style>
body {
width: 600px;
margin: 1em auto;
background: #fff;
font-family: Helvetica, Arial, sans;
}
</style>
</head>
<body>
<h1>Loop UI toolkit</h1>
<h2>Logo icons</h2>
<p>
<img src="../img/icon_32.png"> 32x32 transparent PNG
<img src="../img/icon_64.png"> 64x64 transparent PNG
</p>
<p><em><strong>Note:</strong> these are temporary.</em></p>
<h2>Share panel</h2>
<h3>Simple</h3>
<div class="share">
<form class="description">
<p>This is a simple message.</p>
</form>
<div class="action">
<p><input type="url" value="http://loop.im/plop75"></p>
<p>Your name will appear as <a href="">Unnamed</a>.</p>
</div>
</div>
<h3>Featuring options</h3>
<div class="share">
<form class="description">
<p class="field">
<label>Share this link with a friend to
<select>
<option>browse together</option>
<option selected="selected">video chat</option>
<option>audio chat</option>
<option>text chat</option>
</select>
</label>
</p>
<p class="field">
<label>
Use webcam <select><option>Foo</option></select>
</label>
</p>
<p class="field">
<label>Use whatever
<select><option>Long foo is long indeed</option></select>
</label>
</p>
<p class="preview cf">
Preview <video></video>
</p>
</form>
<div class="action">
<p><input type="url" value="http://loop.im/plop75"></p>
<p>Your name will appear as <a href="">Unnamed</a>.</p>
</div>
</div>
<h2>Conversation window</h2>
<p><em>The conversation component adapts automatically to its container to
occupy all the available space.</em></p>
<h3>Large</h3>
<div class="conversation">
<div class="media nested">
<video class="remote"></video>
<video class="local"></video>
</div>
</div>
<h3>Large with controls</h3>
<div class="conversation">
<nav class="controls">
<button class="btn">Start</button>
<button class="btn">Stop</button>
</nav>
<div class="media nested">
<video class="remote"></video>
<video class="local"></video>
</div>
</div>
<h3>Small (think chat window)</h3>
<div style="width: 204px">
<div class="conversation">
<div class="media nested">
<video class="remote"></video>
<video class="local"></video>
</div>
</div>
</div>
<h3>Side by side</h3>
<div class="conversation">
<div class="media side-by-side">
<video class="remote"></video>
<video class="local"></video>
</div>
</div>
<h2>Buttons</h2>
<h3>Using <code>&lt;a&gt;</code></h3>
<p>
<a href="" class="btn">default</a>
<a href="" class="btn btn-info">info</a>
<a href="" class="btn btn-success">success</a>
<a href="" class="btn btn-warning">warning</a>
<a href="" class="btn btn-error">error</a>
</p>
<h3>Inline</h3>
<p>Click <a href="" class="btn btn-info">here</a>.</p>
<h3>Using <code>&lt;button&gt;</code></h3>
<p>
<button class="btn">default</button>
<button class="btn btn-info">info</button>
<button class="btn btn-success">success</button>
<button class="btn btn-warning">warning</button>
<button class="btn btn-error">error</button>
</p>
<h2>Alerts</h2>
<div class="alert alert-error">
<button class="close"></button>
<p class="message">Oops! Something went really wrong.</p>
</div>
<div class="alert alert-warning">
<button class="close"></button>
<p class="message">Oops! This is a warning.</p>
</div>
<h2>Incoming call</h2>
<div class="incoming-call">
<h2>Incoming call</h2>
<p>
<button class="btn btn-success btn-accept">Accept</button>
<button class="btn btn-error btn-decline">Decline</button>
</p>
</div>
<script>
window.onload = function() {
[].forEach.call(document.querySelectorAll("video"), function(video) {
video.setAttribute("src", "http://v2v.cc/~j/theora_testsuite/320x240.ogg");
});
};
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,362 @@
/* 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

@ -0,0 +1,241 @@
/* 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 */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.models = (function() {
"use strict";
/**
* Conversation model.
*/
var ConversationModel = Backbone.Model.extend({
defaults: {
ongoing: false, // Ongoing call flag
callerId: undefined, // Loop caller id
loopToken: undefined, // Loop conversation token
loopVersion: undefined, // Loop version for /calls/ information. This
// is the version received from the push
// notification and is used by the server to
// determine the pending calls
sessionId: undefined, // OT session id
sessionToken: undefined, // OT session token
apiKey: undefined // OT api key
},
/**
* SDK object.
* @type {OT}
*/
sdk: undefined,
/**
* SDK session object.
* @type {XXX}
*/
session: undefined,
/**
* Constructor.
*
* Required options:
* - {OT} sdk: SDK object.
*
* @param {Object} attributes Attributes object.
* @param {Object} options Options object.
*/
initialize: function(attributes, options) {
options = options || {};
if (!options.sdk) {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
},
/**
* Initiates a conversation, requesting call session information to the Loop
* server and updates appropriately the current model attributes with the
* data.
*
* Available options:
*
* - {String} baseServerUrl The server URL
* - {Boolean} outgoing Set to true if this model represents the
* outgoing call.
*
* Triggered events:
*
* - `session:ready` when the session information have been successfully
* retrieved from the server;
* - `session:error` when the request failed.
*
* @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) {
this.trigger("session:error", new Error(
"Retrieval of session information failed: HTTP " + err));
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 990714 should fix this.
if (!options.outgoing)
sessionData = sessionData[0];
this.setReady(sessionData);
}
if (options.outgoing) {
client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
}
else {
client.requestCallsInfo(this.get("loopVersion"),
handleResult.bind(this));
}
},
/**
* Checks that the session is ready.
*
* @return {Boolean}
*/
isSessionReady: function() {
return !!this.get("sessionId");
},
/**
* Sets session information and triggers the `session:ready` event.
*
* @param {Object} sessionData Conversation session information.
*/
setReady: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey
}).trigger("session:ready", this);
return this;
},
/**
* Starts a SDK session and subscribe to call events.
*/
startSession: function() {
if (!this.isSessionReady()) {
throw new Error("Can't start session as it's not ready");
}
this.session = this.sdk.initSession(this.get("sessionId"));
this.listenTo(this.session, "sessionConnected", this._sessionConnected);
this.listenTo(this.session, "streamCreated", this._streamCreated);
this.listenTo(this.session, "connectionDestroyed",
this._connectionDestroyed);
this.listenTo(this.session, "sessionDisconnected",
this._sessionDisconnected);
this.listenTo(this.session, "networkDisconnected",
this._networkDisconnected);
this.session.connect(this.get("apiKey"), this.get("sessionToken"));
},
/**
* Ends current session.
*/
endSession: function() {
this.session.disconnect();
this.once("session:ended", this.stopListening, this);
this.set("ongoing", false);
},
/**
* Session is created.
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
*
* @param {SessionConnectEvent} event
*/
_sessionConnected: function(event) {
this.trigger("session:connected", event);
this.set("ongoing", true);
},
/**
* New created streams are available.
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
this.trigger("session:stream-created", event);
},
/**
* Local user hung up.
* http://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
*
* @param {SessionDisconnectEvent} event
*/
_sessionDisconnected: function(event) {
this.trigger("session:ended");
this.set("ongoing", false);
},
/**
* Peer hung up. Disconnects local session.
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
*
* @param {ConnectionEvent} event
*/
_connectionDestroyed: function(event) {
this.trigger("session:peer-hungup", {
connectionId: event.connection.connectionId
});
this.endSession();
},
/**
* Network was disconnected.
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
*
* @param {ConnectionEvent} event
*/
_networkDisconnected: function(event) {
this.trigger("session:network-disconnected");
this.endSession();
},
});
/**
* Notification model.
*/
var NotificationModel = Backbone.Model.extend({
defaults: {
level: "info",
message: ""
}
});
/**
* Notification collection
*/
var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel
});
return {
ConversationModel: ConversationModel,
NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel
};
})();

View File

@ -0,0 +1,161 @@
/* 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 */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.router = (function(l10n) {
"use strict";
/**
* Base Router. Allows defining a main active view and ease toggling it when
* the active route changes.
*
* @link http://mikeygee.com/blog/backbone.html
*/
var BaseRouter = Backbone.Router.extend({
/**
* Active view.
* @type {loop.shared.views.BaseView}
*/
_activeView: undefined,
/**
* Notifications dispatcher.
* @type {loop.shared.views.NotificationListView}
*/
_notifier: undefined,
/**
* Constructor.
*
* Required options:
* - {loop.shared.views.NotificationListView} notifier Notifier view.
*
* @param {Object} options Options object.
*/
constructor: function(options) {
options = options || {};
if (!options.notifier) {
throw new Error("missing required notifier");
}
this._notifier = options.notifier;
Backbone.Router.apply(this, arguments);
},
/**
* Loads and render current active view.
*
* @param {loop.shared.views.BaseView} view View.
*/
loadView : function(view) {
if (this._activeView) {
this._activeView.remove();
}
this._activeView = view.render().show();
this.updateView(this._activeView.$el);
},
/**
* Updates main div element with provided contents.
*
* @param {jQuery} $el Element.
*/
updateView: function($el) {
$("#main").html($el);
}
});
/**
* Base conversation router, implementing common behaviors when handling
* a conversation.
*/
var BaseConversationRouter = BaseRouter.extend({
/**
* Current conversation.
* @type {loop.shared.models.ConversationModel}
*/
_conversation: undefined,
/**
* Constructor. Defining it as `constructor` allows implementing an
* `initialize` method in child classes without needing calling this parent
* one. See http://backbonejs.org/#Model-constructor (same for Router)
*
* Required options:
* - {loop.shared.model.ConversationModel} model Conversation model.
*
* @param {Object} options Options object.
*/
constructor: function(options) {
options = options || {};
if (!options.conversation) {
throw new Error("missing required conversation");
}
this._conversation = options.conversation;
this.listenTo(this._conversation, "session:ready", this._onSessionReady);
this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
this.listenTo(this._conversation, "session:peer-hungup",
this._onPeerHungup);
this.listenTo(this._conversation, "session:network-disconnected",
this._onNetworkDisconnected);
BaseRouter.apply(this, arguments);
},
/**
* Starts the call. This method should be overriden.
*/
startCall: function() {},
/**
* Ends the call. This method should be overriden.
*/
endCall: function() {},
/**
* Session is ready.
*/
_onSessionReady: function() {
this.startCall();
},
/**
* Session has ended. Notifies the user and ends the call.
*/
_onSessionEnded: function() {
this._notifier.warnL10n("call_has_ended");
this.endCall();
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*
* @param {Object} event
*/
_onPeerHungup: function() {
this._notifier.warnL10n("peer_ended_conversation");
this.endCall();
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
this._notifier.warnL10n("network_disconnected");
this.endCall();
}
});
return {
BaseRouter: BaseRouter,
BaseConversationRouter: BaseConversationRouter
};
})(document.webL10n || document.mozL10n);

View File

@ -0,0 +1,386 @@
/* 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 */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = (function(_, OT, l10n) {
"use strict";
var sharedModels = loop.shared.models;
/**
* L10n view. Translates resulting view DOM fragment once rendered.
*/
var L10nView = (function() {
var L10nViewImpl = Backbone.View.extend(), // Original View constructor
originalExtend = L10nViewImpl.extend; // Original static extend fn
/**
* Patches View extend() method so we can hook and patch any declared render
* method.
*
* @return {Backbone.View} Extended view with patched render() method.
*/
L10nViewImpl.extend = function() {
var ExtendedView = originalExtend.apply(this, arguments),
originalRender = ExtendedView.prototype.render;
/**
* Wraps original render() method to translate contents once they're
* rendered.
*
* @return {Backbone.View} Extended view instance.
*/
ExtendedView.prototype.render = function() {
if (originalRender) {
originalRender.apply(this, arguments);
l10n.translate(this.el);
}
return this;
};
return ExtendedView;
};
return L10nViewImpl;
})();
/**
* Base view.
*/
var BaseView = L10nView.extend({
/**
* Hides view element.
*
* @return {BaseView}
*/
hide: function() {
this.$el.hide();
return this;
},
/**
* Shows view element.
*
* @return {BaseView}
*/
show: function() {
this.$el.show();
return this;
},
/**
* Base render implementation: renders an attached template if available.
*
* Note: You need to override this if you want to do fancier stuff, eg.
* rendering the template using model data.
*
* @return {BaseView}
*/
render: function() {
if (this.template) {
this.$el.html(this.template());
}
return this;
}
});
/**
* Conversation view.
*/
var ConversationView = BaseView.extend({
className: "conversation",
template: _.template([
'<nav class="controls">',
' <button class="btn stop" data-l10n-id="stop"></button>',
'</nav>',
'<div class="media nested">',
// Both these wrappers are required by the SDK; this is fragile and
// will break if a future version of the SDK updates this generated DOM,
// especially as the SDK seems to actually move wrapped contents into
// their own generated stuff.
' <div class="remote"><div class="incoming"></div></div>',
' <div class="local"><div class="outgoing"></div></div>',
'</div>'
].join("")),
// height set to "auto" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=991122
videoStyles: {
width: "100%",
height: "auto",
style: { "bugDisplayMode": "off" }
},
events: {
'click .btn.stop': 'hangup'
},
/**
* Establishes webrtc communication using OT sdk.
*/
initialize: function(options) {
options = options || {};
if (!options.sdk) {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
this.listenTo(this.model, "session:connected", this.publish);
this.listenTo(this.model, "session:stream-created", this._streamCreated);
this.listenTo(this.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "), this.unpublish);
this.model.startSession();
},
/**
* Subscribes and attaches each created stream to a DOM element.
*
* XXX: for now we only support a single remote stream, hence a single DOM
* element.
*
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
var incoming = this.$(".incoming").get(0);
event.streams.forEach(function(stream) {
if (stream.connection.connectionId !==
this.model.session.connection.connectionId) {
this.model.session.subscribe(stream, incoming, this.videoStyles);
}
}.bind(this));
},
/**
* Hangs up current conversation.
*
* @param {MouseEvent} event
*/
hangup: function(event) {
event.preventDefault();
this.unpublish();
this.model.endSession();
},
/**
* Publishes remote streams available once a session is connected.
*
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
*
* @param {SessionConnectEvent} event
*/
publish: function(event) {
var outgoing = this.$(".outgoing").get(0);
this.publisher = this.sdk.initPublisher(outgoing, this.videoStyles);
// Suppress OT GuM custom dialog, see bug 1018875
function preventOpeningAccessDialog(event) {
event.preventDefault();
}
this.publisher.on("accessDialogOpened", preventOpeningAccessDialog);
this.publisher.on("accessDenied", preventOpeningAccessDialog);
this.model.session.publish(this.publisher);
},
/**
* Unpublishes local stream.
*/
unpublish: function() {
// Unregister access OT GuM custom dialog listeners, see bug 1018875
this.publisher.off("accessDialogOpened");
this.publisher.off("accessDenied");
this.model.session.unpublish(this.publisher);
},
/**
* Renders this view.
*
* @return {ConversationView}
*/
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
/**
* Notification view.
*/
var NotificationView = BaseView.extend({
template: _.template([
'<div class="alert alert-<%- level %>">',
' <button class="close"></button>',
' <p class="message"><%- message %></p>',
'</div>'
].join("")),
events: {
"click .close": "dismiss"
},
dismiss: function(event) {
event.preventDefault();
this.$el.addClass("fade-out");
setTimeout(function() {
this.collection.remove(this.model);
this.remove();
}.bind(this), 500); // XXX make timeout value configurable
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
/**
* Notification list view.
*/
var NotificationListView = Backbone.View.extend({
/**
* Constructor.
*
* Available options:
* - {loop.shared.models.NotificationCollection} collection Notifications
* collection
*
* @param {Object} options Options object
*/
initialize: function(options) {
options = options || {};
if (!options.collection) {
this.collection = new sharedModels.NotificationCollection();
}
this.listenTo(this.collection, "reset add remove", this.render);
},
/**
* Clears the notification stack.
*/
clear: function() {
this.collection.reset();
},
/**
* Adds a new notification to the stack, triggering rendering of it.
*
* @param {Object|NotificationModel} notification Notification data.
*/
notify: function(notification) {
this.collection.add(notification);
},
/**
* Adds a new notification to the stack using an l10n message identifier,
* triggering rendering of it.
*
* @param {String} messageId L10n message id
* @param {String} level Notification level
*/
notifyL10n: function(messageId, level) {
this.notify({
message: l10n.get(messageId),
level: level
});
},
/**
* Adds a warning notification to the stack and renders it.
*
* @return {String} message
*/
warn: function(message) {
this.notify({level: "warning", message: message});
},
/**
* Adds a l10n warning notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
warnL10n: function(messageId) {
this.warn(l10n.get(messageId));
},
/**
* Adds an error notification to the stack and renders it.
*
* @return {String} message
*/
error: function(message) {
this.notify({level: "error", message: message});
},
/**
* Adds a l10n rror notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
errorL10n: function(messageId) {
this.error(l10n.get(messageId));
},
/**
* Renders this view.
*
* @return {loop.shared.views.NotificationListView}
*/
render: function() {
this.$el.html(this.collection.map(function(notification) {
return new NotificationView({
model: notification,
collection: this.collection
}).render().$el;
}.bind(this)));
return this;
}
});
/**
* Unsupported Browsers view.
*/
var UnsupportedBrowserView = BaseView.extend({
template: _.template([
'<div>',
' <h2 data-l10n-id="incompatible_browser"></h2>',
' <p data-l10n-id="powered_by_webrtc"></p>',
' <p data-l10n-id="use_latest_firefox" ',
' data-l10n-args=\'{"ff_url": "https://www.mozilla.org/firefox/"}\'>',
' </p>',
'</div>'
].join(""))
});
/**
* Unsupported Browsers view.
*/
var UnsupportedDeviceView = BaseView.extend({
template: _.template([
'<div>',
' <h2 data-l10n-id="incompatible_device"></h2>',
' <p data-l10n-id="sorry_device_unsupported"></p>',
' <p data-l10n-id="use_firefox_windows_mac_linux"></p>',
'</div>'
].join(""))
});
return {
L10nView: L10nView,
BaseView: BaseView,
ConversationView: ConversationView,
NotificationListView: NotificationListView,
NotificationView: NotificationView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView
};
})(_, window.OT, document.webL10n || document.mozL10n);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,556 @@
/*
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$

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,56 @@
/**
* @license
* Lo-Dash 2.4.1 (Custom Build) lodash.com/license | Underscore.js 1.5.2 underscorejs.org/LICENSE
* Build: `lodash modern -o ./dist/lodash.js`
*/
;(function(){function n(n,t,e){e=(e||0)-1;for(var r=n?n.length:0;++e<r;)if(n[e]===t)return e;return-1}function t(t,e){var r=typeof e;if(t=t.l,"boolean"==r||null==e)return t[e]?0:-1;"number"!=r&&"string"!=r&&(r="object");var u="number"==r?e:m+e;return t=(t=t[r])&&t[u],"object"==r?t&&-1<n(t,e)?0:-1:t?0:-1}function e(n){var t=this.l,e=typeof n;if("boolean"==e||null==n)t[n]=true;else{"number"!=e&&"string"!=e&&(e="object");var r="number"==e?n:m+n,t=t[e]||(t[e]={});"object"==e?(t[r]||(t[r]=[])).push(n):t[r]=true
}}function r(n){return n.charCodeAt(0)}function u(n,t){for(var e=n.m,r=t.m,u=-1,o=e.length;++u<o;){var i=e[u],a=r[u];if(i!==a){if(i>a||typeof i=="undefined")return 1;if(i<a||typeof a=="undefined")return-1}}return n.n-t.n}function o(n){var t=-1,r=n.length,u=n[0],o=n[r/2|0],i=n[r-1];if(u&&typeof u=="object"&&o&&typeof o=="object"&&i&&typeof i=="object")return false;for(u=f(),u["false"]=u["null"]=u["true"]=u.undefined=false,o=f(),o.k=n,o.l=u,o.push=e;++t<r;)o.push(n[t]);return o}function i(n){return"\\"+U[n]
}function a(){return h.pop()||[]}function f(){return g.pop()||{k:null,l:null,m:null,"false":false,n:0,"null":false,number:null,object:null,push:null,string:null,"true":false,undefined:false,o:null}}function l(n){n.length=0,h.length<_&&h.push(n)}function c(n){var t=n.l;t&&c(t),n.k=n.l=n.m=n.object=n.number=n.string=n.o=null,g.length<_&&g.push(n)}function p(n,t,e){t||(t=0),typeof e=="undefined"&&(e=n?n.length:0);var r=-1;e=e-t||0;for(var u=Array(0>e?0:e);++r<e;)u[r]=n[t+r];return u}function s(e){function h(n,t,e){if(!n||!V[typeof n])return n;
t=t&&typeof e=="undefined"?t:tt(t,e,3);for(var r=-1,u=V[typeof n]&&Fe(n),o=u?u.length:0;++r<o&&(e=u[r],false!==t(n[e],e,n)););return n}function g(n,t,e){var r;if(!n||!V[typeof n])return n;t=t&&typeof e=="undefined"?t:tt(t,e,3);for(r in n)if(false===t(n[r],r,n))break;return n}function _(n,t,e){var r,u=n,o=u;if(!u)return o;for(var i=arguments,a=0,f=typeof e=="number"?2:i.length;++a<f;)if((u=i[a])&&V[typeof u])for(var l=-1,c=V[typeof u]&&Fe(u),p=c?c.length:0;++l<p;)r=c[l],"undefined"==typeof o[r]&&(o[r]=u[r]);
return o}function U(n,t,e){var r,u=n,o=u;if(!u)return o;var i=arguments,a=0,f=typeof e=="number"?2:i.length;if(3<f&&"function"==typeof i[f-2])var l=tt(i[--f-1],i[f--],2);else 2<f&&"function"==typeof i[f-1]&&(l=i[--f]);for(;++a<f;)if((u=i[a])&&V[typeof u])for(var c=-1,p=V[typeof u]&&Fe(u),s=p?p.length:0;++c<s;)r=p[c],o[r]=l?l(o[r],u[r]):u[r];return o}function H(n){var t,e=[];if(!n||!V[typeof n])return e;for(t in n)me.call(n,t)&&e.push(t);return e}function J(n){return n&&typeof n=="object"&&!Te(n)&&me.call(n,"__wrapped__")?n:new Q(n)
}function Q(n,t){this.__chain__=!!t,this.__wrapped__=n}function X(n){function t(){if(r){var n=p(r);be.apply(n,arguments)}if(this instanceof t){var o=nt(e.prototype),n=e.apply(o,n||arguments);return wt(n)?n:o}return e.apply(u,n||arguments)}var e=n[0],r=n[2],u=n[4];return $e(t,n),t}function Z(n,t,e,r,u){if(e){var o=e(n);if(typeof o!="undefined")return o}if(!wt(n))return n;var i=ce.call(n);if(!K[i])return n;var f=Ae[i];switch(i){case T:case F:return new f(+n);case W:case P:return new f(n);case z:return o=f(n.source,C.exec(n)),o.lastIndex=n.lastIndex,o
}if(i=Te(n),t){var c=!r;r||(r=a()),u||(u=a());for(var s=r.length;s--;)if(r[s]==n)return u[s];o=i?f(n.length):{}}else o=i?p(n):U({},n);return i&&(me.call(n,"index")&&(o.index=n.index),me.call(n,"input")&&(o.input=n.input)),t?(r.push(n),u.push(o),(i?St:h)(n,function(n,i){o[i]=Z(n,t,e,r,u)}),c&&(l(r),l(u)),o):o}function nt(n){return wt(n)?ke(n):{}}function tt(n,t,e){if(typeof n!="function")return Ut;if(typeof t=="undefined"||!("prototype"in n))return n;var r=n.__bindData__;if(typeof r=="undefined"&&(De.funcNames&&(r=!n.name),r=r||!De.funcDecomp,!r)){var u=ge.call(n);
De.funcNames||(r=!O.test(u)),r||(r=E.test(u),$e(n,r))}if(false===r||true!==r&&1&r[1])return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,r){return n.call(t,e,r)};case 3:return function(e,r,u){return n.call(t,e,r,u)};case 4:return function(e,r,u,o){return n.call(t,e,r,u,o)}}return Mt(n,t)}function et(n){function t(){var n=f?i:this;if(u){var h=p(u);be.apply(h,arguments)}return(o||c)&&(h||(h=p(arguments)),o&&be.apply(h,o),c&&h.length<a)?(r|=16,et([e,s?r:-4&r,h,null,i,a])):(h||(h=arguments),l&&(e=n[v]),this instanceof t?(n=nt(e.prototype),h=e.apply(n,h),wt(h)?h:n):e.apply(n,h))
}var e=n[0],r=n[1],u=n[2],o=n[3],i=n[4],a=n[5],f=1&r,l=2&r,c=4&r,s=8&r,v=e;return $e(t,n),t}function rt(e,r){var u=-1,i=st(),a=e?e.length:0,f=a>=b&&i===n,l=[];if(f){var p=o(r);p?(i=t,r=p):f=false}for(;++u<a;)p=e[u],0>i(r,p)&&l.push(p);return f&&c(r),l}function ut(n,t,e,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r<u;){var i=n[r];if(i&&typeof i=="object"&&typeof i.length=="number"&&(Te(i)||yt(i))){t||(i=ut(i,t,e));var a=-1,f=i.length,l=o.length;for(o.length+=f;++a<f;)o[l++]=i[a]}else e||o.push(i)}return o
}function ot(n,t,e,r,u,o){if(e){var i=e(n,t);if(typeof i!="undefined")return!!i}if(n===t)return 0!==n||1/n==1/t;if(n===n&&!(n&&V[typeof n]||t&&V[typeof t]))return false;if(null==n||null==t)return n===t;var f=ce.call(n),c=ce.call(t);if(f==D&&(f=q),c==D&&(c=q),f!=c)return false;switch(f){case T:case F:return+n==+t;case W:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case z:case P:return n==oe(t)}if(c=f==$,!c){var p=me.call(n,"__wrapped__"),s=me.call(t,"__wrapped__");if(p||s)return ot(p?n.__wrapped__:n,s?t.__wrapped__:t,e,r,u,o);
if(f!=q)return false;if(f=n.constructor,p=t.constructor,f!=p&&!(dt(f)&&f instanceof f&&dt(p)&&p instanceof p)&&"constructor"in n&&"constructor"in t)return false}for(f=!u,u||(u=a()),o||(o=a()),p=u.length;p--;)if(u[p]==n)return o[p]==t;var v=0,i=true;if(u.push(n),o.push(t),c){if(p=n.length,v=t.length,(i=v==p)||r)for(;v--;)if(c=p,s=t[v],r)for(;c--&&!(i=ot(n[c],s,e,r,u,o)););else if(!(i=ot(n[v],s,e,r,u,o)))break}else g(t,function(t,a,f){return me.call(f,a)?(v++,i=me.call(n,a)&&ot(n[a],t,e,r,u,o)):void 0}),i&&!r&&g(n,function(n,t,e){return me.call(e,t)?i=-1<--v:void 0
});return u.pop(),o.pop(),f&&(l(u),l(o)),i}function it(n,t,e,r,u){(Te(t)?St:h)(t,function(t,o){var i,a,f=t,l=n[o];if(t&&((a=Te(t))||Pe(t))){for(f=r.length;f--;)if(i=r[f]==t){l=u[f];break}if(!i){var c;e&&(f=e(l,t),c=typeof f!="undefined")&&(l=f),c||(l=a?Te(l)?l:[]:Pe(l)?l:{}),r.push(t),u.push(l),c||it(l,t,e,r,u)}}else e&&(f=e(l,t),typeof f=="undefined"&&(f=t)),typeof f!="undefined"&&(l=f);n[o]=l})}function at(n,t){return n+he(Re()*(t-n+1))}function ft(e,r,u){var i=-1,f=st(),p=e?e.length:0,s=[],v=!r&&p>=b&&f===n,h=u||v?a():s;
for(v&&(h=o(h),f=t);++i<p;){var g=e[i],y=u?u(g,i,e):g;(r?!i||h[h.length-1]!==y:0>f(h,y))&&((u||v)&&h.push(y),s.push(g))}return v?(l(h.k),c(h)):u&&l(h),s}function lt(n){return function(t,e,r){var u={};e=J.createCallback(e,r,3),r=-1;var o=t?t.length:0;if(typeof o=="number")for(;++r<o;){var i=t[r];n(u,i,e(i,r,t),t)}else h(t,function(t,r,o){n(u,t,e(t,r,o),o)});return u}}function ct(n,t,e,r,u,o){var i=1&t,a=4&t,f=16&t,l=32&t;if(!(2&t||dt(n)))throw new ie;f&&!e.length&&(t&=-17,f=e=false),l&&!r.length&&(t&=-33,l=r=false);
var c=n&&n.__bindData__;return c&&true!==c?(c=p(c),c[2]&&(c[2]=p(c[2])),c[3]&&(c[3]=p(c[3])),!i||1&c[1]||(c[4]=u),!i&&1&c[1]&&(t|=8),!a||4&c[1]||(c[5]=o),f&&be.apply(c[2]||(c[2]=[]),e),l&&we.apply(c[3]||(c[3]=[]),r),c[1]|=t,ct.apply(null,c)):(1==t||17===t?X:et)([n,t,e,r,u,o])}function pt(n){return Be[n]}function st(){var t=(t=J.indexOf)===Wt?n:t;return t}function vt(n){return typeof n=="function"&&pe.test(n)}function ht(n){var t,e;return n&&ce.call(n)==q&&(t=n.constructor,!dt(t)||t instanceof t)?(g(n,function(n,t){e=t
}),typeof e=="undefined"||me.call(n,e)):false}function gt(n){return We[n]}function yt(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==D||false}function mt(n,t,e){var r=Fe(n),u=r.length;for(t=tt(t,e,3);u--&&(e=r[u],false!==t(n[e],e,n)););return n}function bt(n){var t=[];return g(n,function(n,e){dt(n)&&t.push(e)}),t.sort()}function _t(n){for(var t=-1,e=Fe(n),r=e.length,u={};++t<r;){var o=e[t];u[n[o]]=o}return u}function dt(n){return typeof n=="function"}function wt(n){return!(!n||!V[typeof n])
}function jt(n){return typeof n=="number"||n&&typeof n=="object"&&ce.call(n)==W||false}function kt(n){return typeof n=="string"||n&&typeof n=="object"&&ce.call(n)==P||false}function xt(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;)u[t]=n[e[t]];return u}function Ct(n,t,e){var r=-1,u=st(),o=n?n.length:0,i=false;return e=(0>e?Ie(0,o+e):e)||0,Te(n)?i=-1<u(n,t,e):typeof o=="number"?i=-1<(kt(n)?n.indexOf(t,e):u(n,t,e)):h(n,function(n){return++r<e?void 0:!(i=n===t)}),i}function Ot(n,t,e){var r=true;t=J.createCallback(t,e,3),e=-1;
var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&(r=!!t(n[e],e,n)););else h(n,function(n,e,u){return r=!!t(n,e,u)});return r}function Nt(n,t,e){var r=[];t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u;){var o=n[e];t(o,e,n)&&r.push(o)}else h(n,function(n,e,u){t(n,e,u)&&r.push(n)});return r}function It(n,t,e){t=J.createCallback(t,e,3),e=-1;var r=n?n.length:0;if(typeof r!="number"){var u;return h(n,function(n,e,r){return t(n,e,r)?(u=n,false):void 0}),u}for(;++e<r;){var o=n[e];
if(t(o,e,n))return o}}function St(n,t,e){var r=-1,u=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof u=="number")for(;++r<u&&false!==t(n[r],r,n););else h(n,t);return n}function Et(n,t,e){var r=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof r=="number")for(;r--&&false!==t(n[r],r,n););else{var u=Fe(n),r=u.length;h(n,function(n,e,o){return e=u?u[--r]:--r,t(o[e],e,o)})}return n}function Rt(n,t,e){var r=-1,u=n?n.length:0;if(t=J.createCallback(t,e,3),typeof u=="number")for(var o=Xt(u);++r<u;)o[r]=t(n[r],r,n);
else o=[],h(n,function(n,e,u){o[++r]=t(n,e,u)});return o}function At(n,t,e){var u=-1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a>o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e>u&&(u=e,o=n)});return o}function Dt(n,t,e,r){if(!n)return e;var u=3>arguments.length;t=J.createCallback(t,r,4);var o=-1,i=n.length;if(typeof i=="number")for(u&&(e=n[++o]);++o<i;)e=t(e,n[o],o,n);else h(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)
});return e}function $t(n,t,e,r){var u=3>arguments.length;return t=J.createCallback(t,r,4),Et(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)}),e}function Tt(n){var t=-1,e=n?n.length:0,r=Xt(typeof e=="number"?e:0);return St(n,function(n){var e=at(0,++t);r[t]=r[e],r[e]=n}),r}function Ft(n,t,e){var r;t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&!(r=t(n[e],e,n)););else h(n,function(n,e,u){return!(r=t(n,e,u))});return!!r}function Bt(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=-1;
for(t=J.createCallback(t,e,3);++o<u&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[0]:v;return p(n,0,Se(Ie(0,r),u))}function Wt(t,e,r){if(typeof r=="number"){var u=t?t.length:0;r=0>r?Ie(0,u+r):r||0}else if(r)return r=zt(t,e),t[r]===e?r:-1;return n(t,e,r)}function qt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=J.createCallback(t,e,3);++u<o&&t(n[u],u,n);)r++}else r=null==t||e?1:Ie(0,t);return p(n,r)}function zt(n,t,e,r){var u=0,o=n?n.length:u;for(e=e?J.createCallback(e,r,1):Ut,t=e(t);u<o;)r=u+o>>>1,e(n[r])<t?u=r+1:o=r;
return u}function Pt(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(e=J.createCallback(e,r,3)),ft(n,t,e)}function Kt(){for(var n=1<arguments.length?arguments:arguments[0],t=-1,e=n?At(Ve(n,"length")):0,r=Xt(0>e?0:e);++t<e;)r[t]=Ve(n,t);return r}function Lt(n,t){var e=-1,r=n?n.length:0,u={};for(t||!r||Te(n[0])||(t=[]);++e<r;){var o=n[e];t?u[o]=t[e]:o&&(u[o[0]]=o[1])}return u}function Mt(n,t){return 2<arguments.length?ct(n,17,p(arguments,2),null,t):ct(n,1,null,null,t)
}function Vt(n,t,e){function r(){c&&ve(c),i=c=p=v,(g||h!==t)&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null))}function u(){var e=t-(Ue()-f);0<e?c=_e(u,e):(i&&ve(i),e=p,i=c=p=v,e&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null)))}var o,i,a,f,l,c,p,s=0,h=false,g=true;if(!dt(n))throw new ie;if(t=Ie(0,t)||0,true===e)var y=true,g=false;else wt(e)&&(y=e.leading,h="maxWait"in e&&(Ie(t,e.maxWait)||0),g="trailing"in e?e.trailing:g);return function(){if(o=arguments,f=Ue(),l=this,p=g&&(c||!y),false===h)var e=y&&!c;else{i||y||(s=f);var v=h-(f-s),m=0>=v;
m?(i&&(i=ve(i)),s=f,a=n.apply(l,o)):i||(i=_e(r,v))}return m&&c?c=ve(c):c||t===h||(c=_e(u,t)),e&&(m=true,a=n.apply(l,o)),!m||c||i||(o=l=null),a}}function Ut(n){return n}function Gt(n,t,e){var r=true,u=t&&bt(t);t&&(e||u.length)||(null==e&&(e=t),o=Q,t=n,n=J,u=bt(t)),false===e?r=false:wt(e)&&"chain"in e&&(r=e.chain);var o=n,i=dt(o);St(u,function(e){var u=n[e]=t[e];i&&(o.prototype[e]=function(){var t=this.__chain__,e=this.__wrapped__,i=[e];if(be.apply(i,arguments),i=u.apply(n,i),r||t){if(e===i&&wt(i))return this;
i=new o(i),i.__chain__=t}return i})})}function Ht(){}function Jt(n){return function(t){return t[n]}}function Qt(){return this.__wrapped__}e=e?Y.defaults(G.Object(),e,Y.pick(G,A)):G;var Xt=e.Array,Yt=e.Boolean,Zt=e.Date,ne=e.Function,te=e.Math,ee=e.Number,re=e.Object,ue=e.RegExp,oe=e.String,ie=e.TypeError,ae=[],fe=re.prototype,le=e._,ce=fe.toString,pe=ue("^"+oe(ce).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),se=te.ceil,ve=e.clearTimeout,he=te.floor,ge=ne.prototype.toString,ye=vt(ye=re.getPrototypeOf)&&ye,me=fe.hasOwnProperty,be=ae.push,_e=e.setTimeout,de=ae.splice,we=ae.unshift,je=function(){try{var n={},t=vt(t=re.defineProperty)&&t,e=t(n,n,n)&&t
}catch(r){}return e}(),ke=vt(ke=re.create)&&ke,xe=vt(xe=Xt.isArray)&&xe,Ce=e.isFinite,Oe=e.isNaN,Ne=vt(Ne=re.keys)&&Ne,Ie=te.max,Se=te.min,Ee=e.parseInt,Re=te.random,Ae={};Ae[$]=Xt,Ae[T]=Yt,Ae[F]=Zt,Ae[B]=ne,Ae[q]=re,Ae[W]=ee,Ae[z]=ue,Ae[P]=oe,Q.prototype=J.prototype;var De=J.support={};De.funcDecomp=!vt(e.a)&&E.test(s),De.funcNames=typeof ne.name=="string",J.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:N,variable:"",imports:{_:J}},ke||(nt=function(){function n(){}return function(t){if(wt(t)){n.prototype=t;
var r=new n;n.prototype=null}return r||e.Object()}}());var $e=je?function(n,t){M.value=t,je(n,"__bindData__",M)}:Ht,Te=xe||function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==$||false},Fe=Ne?function(n){return wt(n)?Ne(n):[]}:H,Be={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},We=_t(Be),qe=ue("("+Fe(We).join("|")+")","g"),ze=ue("["+Fe(Be).join("")+"]","g"),Pe=ye?function(n){if(!n||ce.call(n)!=q)return false;var t=n.valueOf,e=vt(t)&&(e=ye(t))&&ye(e);return e?n==e||ye(n)==e:ht(n)
}:ht,Ke=lt(function(n,t,e){me.call(n,e)?n[e]++:n[e]=1}),Le=lt(function(n,t,e){(me.call(n,e)?n[e]:n[e]=[]).push(t)}),Me=lt(function(n,t,e){n[e]=t}),Ve=Rt,Ue=vt(Ue=Zt.now)&&Ue||function(){return(new Zt).getTime()},Ge=8==Ee(d+"08")?Ee:function(n,t){return Ee(kt(n)?n.replace(I,""):n,t||0)};return J.after=function(n,t){if(!dt(t))throw new ie;return function(){return 1>--n?t.apply(this,arguments):void 0}},J.assign=U,J.at=function(n){for(var t=arguments,e=-1,r=ut(t,true,false,1),t=t[2]&&t[2][t[1]]===n?1:r.length,u=Xt(t);++e<t;)u[e]=n[r[e]];
return u},J.bind=Mt,J.bindAll=function(n){for(var t=1<arguments.length?ut(arguments,true,false,1):bt(n),e=-1,r=t.length;++e<r;){var u=t[e];n[u]=ct(n[u],1,null,null,n)}return n},J.bindKey=function(n,t){return 2<arguments.length?ct(t,19,p(arguments,2),null,n):ct(t,3,null,null,n)},J.chain=function(n){return n=new Q(n),n.__chain__=true,n},J.compact=function(n){for(var t=-1,e=n?n.length:0,r=[];++t<e;){var u=n[t];u&&r.push(u)}return r},J.compose=function(){for(var n=arguments,t=n.length;t--;)if(!dt(n[t]))throw new ie;
return function(){for(var t=arguments,e=n.length;e--;)t=[n[e].apply(this,t)];return t[0]}},J.constant=function(n){return function(){return n}},J.countBy=Ke,J.create=function(n,t){var e=nt(n);return t?U(e,t):e},J.createCallback=function(n,t,e){var r=typeof n;if(null==n||"function"==r)return tt(n,t,e);if("object"!=r)return Jt(n);var u=Fe(n),o=u[0],i=n[o];return 1!=u.length||i!==i||wt(i)?function(t){for(var e=u.length,r=false;e--&&(r=ot(t[u[e]],n[u[e]],null,true)););return r}:function(n){return n=n[o],i===n&&(0!==i||1/i==1/n)
}},J.curry=function(n,t){return t=typeof t=="number"?t:+t||n.length,ct(n,4,null,null,null,t)},J.debounce=Vt,J.defaults=_,J.defer=function(n){if(!dt(n))throw new ie;var t=p(arguments,1);return _e(function(){n.apply(v,t)},1)},J.delay=function(n,t){if(!dt(n))throw new ie;var e=p(arguments,2);return _e(function(){n.apply(v,e)},t)},J.difference=function(n){return rt(n,ut(arguments,true,true,1))},J.filter=Nt,J.flatten=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(n=Rt(n,e,r)),ut(n,t)
},J.forEach=St,J.forEachRight=Et,J.forIn=g,J.forInRight=function(n,t,e){var r=[];g(n,function(n,t){r.push(t,n)});var u=r.length;for(t=tt(t,e,3);u--&&false!==t(r[u--],r[u],n););return n},J.forOwn=h,J.forOwnRight=mt,J.functions=bt,J.groupBy=Le,J.indexBy=Me,J.initial=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else r=null==t||e?1:t||r;return p(n,0,Se(Ie(0,u-r),u))},J.intersection=function(){for(var e=[],r=-1,u=arguments.length,i=a(),f=st(),p=f===n,s=a();++r<u;){var v=arguments[r];
(Te(v)||yt(v))&&(e.push(v),i.push(p&&v.length>=b&&o(r?e[r]:s)))}var p=e[0],h=-1,g=p?p.length:0,y=[];n:for(;++h<g;){var m=i[0],v=p[h];if(0>(m?t(m,v):f(s,v))){for(r=u,(m||s).push(v);--r;)if(m=i[r],0>(m?t(m,v):f(e[r],v)))continue n;y.push(v)}}for(;u--;)(m=i[u])&&c(m);return l(i),l(s),y},J.invert=_t,J.invoke=function(n,t){var e=p(arguments,2),r=-1,u=typeof t=="function",o=n?n.length:0,i=Xt(typeof o=="number"?o:0);return St(n,function(n){i[++r]=(u?t:n[t]).apply(n,e)}),i},J.keys=Fe,J.map=Rt,J.mapValues=function(n,t,e){var r={};
return t=J.createCallback(t,e,3),h(n,function(n,e,u){r[e]=t(n,e,u)}),r},J.max=At,J.memoize=function(n,t){function e(){var r=e.cache,u=t?t.apply(this,arguments):m+arguments[0];return me.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!dt(n))throw new ie;return e.cache={},e},J.merge=function(n){var t=arguments,e=2;if(!wt(n))return n;if("number"!=typeof t[2]&&(e=t.length),3<e&&"function"==typeof t[e-2])var r=tt(t[--e-1],t[e--],2);else 2<e&&"function"==typeof t[e-1]&&(r=t[--e]);for(var t=p(arguments,1,e),u=-1,o=a(),i=a();++u<e;)it(n,t[u],r,o,i);
return l(o),l(i),n},J.min=function(n,t,e){var u=1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a<o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e<u&&(u=e,o=n)});return o},J.omit=function(n,t,e){var r={};if(typeof t!="function"){var u=[];g(n,function(n,t){u.push(t)});for(var u=rt(u,ut(arguments,true,false,1)),o=-1,i=u.length;++o<i;){var a=u[o];r[a]=n[a]}}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)||(r[e]=n)
});return r},J.once=function(n){var t,e;if(!dt(n))throw new ie;return function(){return t?e:(t=true,e=n.apply(this,arguments),n=null,e)}},J.pairs=function(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;){var o=e[t];u[t]=[o,n[o]]}return u},J.partial=function(n){return ct(n,16,p(arguments,1))},J.partialRight=function(n){return ct(n,32,null,p(arguments,1))},J.pick=function(n,t,e){var r={};if(typeof t!="function")for(var u=-1,o=ut(arguments,true,false,1),i=wt(n)?o.length:0;++u<i;){var a=o[u];a in n&&(r[a]=n[a])
}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)&&(r[e]=n)});return r},J.pluck=Ve,J.property=Jt,J.pull=function(n){for(var t=arguments,e=0,r=t.length,u=n?n.length:0;++e<r;)for(var o=-1,i=t[e];++o<u;)n[o]===i&&(de.call(n,o--,1),u--);return n},J.range=function(n,t,e){n=+n||0,e=typeof e=="number"?e:+e||1,null==t&&(t=n,n=0);var r=-1;t=Ie(0,se((t-n)/(e||1)));for(var u=Xt(t);++r<t;)u[r]=n,n+=e;return u},J.reject=function(n,t,e){return t=J.createCallback(t,e,3),Nt(n,function(n,e,r){return!t(n,e,r)
})},J.remove=function(n,t,e){var r=-1,u=n?n.length:0,o=[];for(t=J.createCallback(t,e,3);++r<u;)e=n[r],t(e,r,n)&&(o.push(e),de.call(n,r--,1),u--);return o},J.rest=qt,J.shuffle=Tt,J.sortBy=function(n,t,e){var r=-1,o=Te(t),i=n?n.length:0,p=Xt(typeof i=="number"?i:0);for(o||(t=J.createCallback(t,e,3)),St(n,function(n,e,u){var i=p[++r]=f();o?i.m=Rt(t,function(t){return n[t]}):(i.m=a())[0]=t(n,e,u),i.n=r,i.o=n}),i=p.length,p.sort(u);i--;)n=p[i],p[i]=n.o,o||l(n.m),c(n);return p},J.tap=function(n,t){return t(n),n
},J.throttle=function(n,t,e){var r=true,u=true;if(!dt(n))throw new ie;return false===e?r=false:wt(e)&&(r="leading"in e?e.leading:r,u="trailing"in e?e.trailing:u),L.leading=r,L.maxWait=t,L.trailing=u,Vt(n,t,L)},J.times=function(n,t,e){n=-1<(n=+n)?n:0;var r=-1,u=Xt(n);for(t=tt(t,e,1);++r<n;)u[r]=t(r);return u},J.toArray=function(n){return n&&typeof n.length=="number"?p(n):xt(n)},J.transform=function(n,t,e,r){var u=Te(n);if(null==e)if(u)e=[];else{var o=n&&n.constructor;e=nt(o&&o.prototype)}return t&&(t=J.createCallback(t,r,4),(u?St:h)(n,function(n,r,u){return t(e,n,r,u)
})),e},J.union=function(){return ft(ut(arguments,true,true))},J.uniq=Pt,J.values=xt,J.where=Nt,J.without=function(n){return rt(n,p(arguments,1))},J.wrap=function(n,t){return ct(t,16,[n])},J.xor=function(){for(var n=-1,t=arguments.length;++n<t;){var e=arguments[n];if(Te(e)||yt(e))var r=r?ft(rt(r,e).concat(rt(e,r))):e}return r||[]},J.zip=Kt,J.zipObject=Lt,J.collect=Rt,J.drop=qt,J.each=St,J.eachRight=Et,J.extend=U,J.methods=bt,J.object=Lt,J.select=Nt,J.tail=qt,J.unique=Pt,J.unzip=Kt,Gt(J),J.clone=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=t,t=false),Z(n,t,typeof e=="function"&&tt(e,r,1))
},J.cloneDeep=function(n,t,e){return Z(n,true,typeof t=="function"&&tt(t,e,1))},J.contains=Ct,J.escape=function(n){return null==n?"":oe(n).replace(ze,pt)},J.every=Ot,J.find=It,J.findIndex=function(n,t,e){var r=-1,u=n?n.length:0;for(t=J.createCallback(t,e,3);++r<u;)if(t(n[r],r,n))return r;return-1},J.findKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),h(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.findLast=function(n,t,e){var r;return t=J.createCallback(t,e,3),Et(n,function(n,e,u){return t(n,e,u)?(r=n,false):void 0
}),r},J.findLastIndex=function(n,t,e){var r=n?n.length:0;for(t=J.createCallback(t,e,3);r--;)if(t(n[r],r,n))return r;return-1},J.findLastKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),mt(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.has=function(n,t){return n?me.call(n,t):false},J.identity=Ut,J.indexOf=Wt,J.isArguments=yt,J.isArray=Te,J.isBoolean=function(n){return true===n||false===n||n&&typeof n=="object"&&ce.call(n)==T||false},J.isDate=function(n){return n&&typeof n=="object"&&ce.call(n)==F||false
},J.isElement=function(n){return n&&1===n.nodeType||false},J.isEmpty=function(n){var t=true;if(!n)return t;var e=ce.call(n),r=n.length;return e==$||e==P||e==D||e==q&&typeof r=="number"&&dt(n.splice)?!r:(h(n,function(){return t=false}),t)},J.isEqual=function(n,t,e,r){return ot(n,t,typeof e=="function"&&tt(e,r,2))},J.isFinite=function(n){return Ce(n)&&!Oe(parseFloat(n))},J.isFunction=dt,J.isNaN=function(n){return jt(n)&&n!=+n},J.isNull=function(n){return null===n},J.isNumber=jt,J.isObject=wt,J.isPlainObject=Pe,J.isRegExp=function(n){return n&&typeof n=="object"&&ce.call(n)==z||false
},J.isString=kt,J.isUndefined=function(n){return typeof n=="undefined"},J.lastIndexOf=function(n,t,e){var r=n?n.length:0;for(typeof e=="number"&&(r=(0>e?Ie(0,r+e):Se(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},J.mixin=Gt,J.noConflict=function(){return e._=le,this},J.noop=Ht,J.now=Ue,J.parseInt=Ge,J.random=function(n,t,e){var r=null==n,u=null==t;return null==e&&(typeof n=="boolean"&&u?(e=n,n=1):u||typeof t!="boolean"||(e=t,u=true)),r&&u&&(t=1),n=+n||0,u?(t=n,n=0):t=+t||0,e||n%1||t%1?(e=Re(),Se(n+e*(t-n+parseFloat("1e-"+((e+"").length-1))),t)):at(n,t)
},J.reduce=Dt,J.reduceRight=$t,J.result=function(n,t){if(n){var e=n[t];return dt(e)?n[t]():e}},J.runInContext=s,J.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:Fe(n).length},J.some=Ft,J.sortedIndex=zt,J.template=function(n,t,e){var r=J.templateSettings;n=oe(n||""),e=_({},e,r);var u,o=_({},e.imports,r.imports),r=Fe(o),o=xt(o),a=0,f=e.interpolate||S,l="__p+='",f=ue((e.escape||S).source+"|"+f.source+"|"+(f===N?x:S).source+"|"+(e.evaluate||S).source+"|$","g");n.replace(f,function(t,e,r,o,f,c){return r||(r=o),l+=n.slice(a,c).replace(R,i),e&&(l+="'+__e("+e+")+'"),f&&(u=true,l+="';"+f+";\n__p+='"),r&&(l+="'+((__t=("+r+"))==null?'':__t)+'"),a=c+t.length,t
}),l+="';",f=e=e.variable,f||(e="obj",l="with("+e+"){"+l+"}"),l=(u?l.replace(w,""):l).replace(j,"$1").replace(k,"$1;"),l="function("+e+"){"+(f?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}";try{var c=ne(r,"return "+l).apply(v,o)}catch(p){throw p.source=l,p}return t?c(t):(c.source=l,c)},J.unescape=function(n){return null==n?"":oe(n).replace(qe,gt)},J.uniqueId=function(n){var t=++y;return oe(null==n?"":n)+t
},J.all=Ot,J.any=Ft,J.detect=It,J.findWhere=It,J.foldl=Dt,J.foldr=$t,J.include=Ct,J.inject=Dt,Gt(function(){var n={};return h(J,function(t,e){J.prototype[e]||(n[e]=t)}),n}(),false),J.first=Bt,J.last=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[u-1]:v;return p(n,Ie(0,u-r))},J.sample=function(n,t,e){return n&&typeof n.length!="number"&&(n=xt(n)),null==t||e?n?n[at(0,n.length-1)]:v:(n=Tt(n),n.length=Se(Ie(0,t),n.length),n)
},J.take=Bt,J.head=Bt,h(J,function(n,t){var e="sample"!==t;J.prototype[t]||(J.prototype[t]=function(t,r){var u=this.__chain__,o=n(this.__wrapped__,t,r);return u||null!=t&&(!r||e&&typeof t=="function")?new Q(o,u):o})}),J.VERSION="2.4.1",J.prototype.chain=function(){return this.__chain__=true,this},J.prototype.toString=function(){return oe(this.__wrapped__)},J.prototype.value=Qt,J.prototype.valueOf=Qt,St(["join","pop","shift"],function(n){var t=ae[n];J.prototype[n]=function(){var n=this.__chain__,e=t.apply(this.__wrapped__,arguments);
return n?new Q(e,n):e}}),St(["push","reverse","sort","unshift"],function(n){var t=ae[n];J.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),St(["concat","slice","splice"],function(n){var t=ae[n];J.prototype[n]=function(){return new Q(t.apply(this.__wrapped__,arguments),this.__chain__)}}),J}var v,h=[],g=[],y=0,m=+new Date+"",b=75,_=40,d=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",w=/\b__p\+='';/g,j=/\b(__p\+=)''\+/g,k=/(__e\(.*?\)|\b__t\))\+'';/g,x=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,C=/\w*$/,O=/^\s*function[ \n\r\t]+\w/,N=/<%=([\s\S]+?)%>/g,I=RegExp("^["+d+"]*0+(?=.$)"),S=/($^)/,E=/\bthis\b/,R=/['\n\r\t\u2028\u2029\\]/g,A="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),D="[object Arguments]",$="[object Array]",T="[object Boolean]",F="[object Date]",B="[object Function]",W="[object Number]",q="[object Object]",z="[object RegExp]",P="[object String]",K={};
K[B]=false,K[D]=K[$]=K[T]=K[F]=K[W]=K[q]=K[z]=K[P]=true;var L={leading:false,maxWait:0,trailing:false},M={configurable:false,enumerable:false,value:null,writable:false},V={"boolean":false,"function":true,object:true,number:false,string:false,undefined:false},U={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},G=V[typeof window]&&window||this,H=V[typeof exports]&&exports&&!exports.nodeType&&exports,J=V[typeof module]&&module&&!module.nodeType&&module,Q=J&&J.exports===H&&H,X=V[typeof global]&&global;!X||X.global!==X&&X.window!==X||(G=X);
var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G._=Y, define(function(){return Y})):H&&J?Q?(J.exports=Y)._=Y:H._=Y:G._=Y}).call(this);

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,28 @@
/**
* @license OpenTok JavaScript Library v2.2.5
* http://www.tokbox.com/
*
* Copyright (c) 2014 TokBox, Inc.
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
* Date: May 22 07:14:18 2014
*/
!(function() {
TB.Config.replaceWith({
global: {
exceptionLogging: {
enabled: true,
messageLimitPerPartner: 100
},
iceServers: {
enabled: false
},
},
partners: {
}
});
})(TB);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,606 @@
/** @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

@ -0,0 +1,78 @@
/* 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
});
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
# 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/.
browser.jar:
content/browser/loop/conversation.html (content/conversation.html)
content/browser/loop/panel.html (content/panel.html)
content/browser/loop/shared/css/common.css (content/shared/css/common.css)
content/browser/loop/shared/css/panel.css (content/shared/css/panel.css)
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
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/libs/l10n.js (content/libs/l10n.js)
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/panel.js (content/js/panel.js)
# Partner SDK assets
content/browser/loop/shared/libs/sdk.js (content/shared/libs/sdk.js)
content/browser/loop/otcdn/webrtc/v2.2.5/css/ot.min.css (content/shared/libs/otcdn/webrtc/v2.2.5/css/ot.min.css)
content/browser/loop/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js (content/shared/libs/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/audioonly-subscriber.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/audioonly-subscriber.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/buttons.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/buttons.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/loader.gif (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/loader.gif)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/mic-off.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/mic-off.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/mic-on.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/mic-on.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/speaker-off.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/speaker-off.png)
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/speaker-on.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/speaker-on.png)

View File

@ -0,0 +1,22 @@
; This file is in the main loop directory, and not the test directory as we need
; to include the content/ files in what is packaged as test support files.
;
; Although the content/ files would normally be tested from within the browser,
; the issue is that they are seen to be loaded in a "chrome" privilaged context,
; and hence are not allowed to be loaded via the content files that run the
; tests.
;
; Hence we duplicate the same files as support files, so that they can be loaded
; in a content context.
;
; This might get messy if we start having pre-processed files, but for now it
; seems to work.
[DEFAULT]
b2g = false
browser = true
qemu = false
[test/shared/test_shared_all.py]
[test/desktop-local/test_desktop_all.py]
[test/standalone/test_standalone_all.py]

View File

@ -0,0 +1,16 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
JAR_MANIFESTS += ['jar.mn']
JS_MODULES_PATH = 'modules/loop'
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES += [
'MozLoopAPI.jsm',
'MozLoopService.jsm',
]

View File

@ -0,0 +1,2 @@
node_modules
*.pyc

View File

@ -0,0 +1,4 @@
node_modules
content/shared/libs
test/shared/vendor

View File

@ -0,0 +1,43 @@
{
"browser" : true, // Web Browser (window, document, etc)
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"devel" : true, // Development/debugging (alert, confirm, etc)
"immed" : false, // true: Require immediate invocations to be wrapped
// in parens e.g. `(function () { } ());`
"indent" : 2,
"jquery": true,
"latedef" : true, // true: Require variables to be defined before use
// (helps avoid hoisting bugs)
"maxlen" : 80, // {int} Max number of characters per line
"node" : true, // Node.js
"strict": true, // Strict mode
"trailing" : false, // true: Prohibit trailing whitespaces
"undef" : true, // true: Require all non-global variables to be
// declared (prevents global leaks)
"unused" : "vars", // vars: Require all defined variables be used, but not
// function parameters
"globals": {
// chai global
"chai": false,
// mocha globals
"after": false,
"afterEach": false,
"before": false,
"beforeEach": false,
"describe": false,
"it": false,
// firefox stuff not covered by "browser" option
"indexedDB": true,
"mozRTCIceCandidate": false,
"mozRTCPeerConnection": false,
"mozRTCSessionDescription": false,
"Backbone": false,
"_": false
}
}

View File

@ -0,0 +1,24 @@
# 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/.
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
NODE_LOCAL_BIN=./node_modules/.bin
install:
@npm install
test:
@echo "Not implemented yet."
lint:
@$(NODE_LOCAL_BIN)/jshint *.js content test
runserver: config
@node server.js
frontend:
@echo "Not implemented yet."
config:
@echo "var loop = loop || {};\nloop.config = {serverUrl: '`echo $(LOOP_SERVER_URL)`'};" > content/config.js

View File

@ -0,0 +1,50 @@
Loop Client
===========
Prerequisites
-------------
NodeJS and npm installed.
Installation
------------
$ make install
Configuration
-------------
You will need to generate a configuration file, you can do so with:
$ make config
It will read the configuration from the `LOOP_SERVER_URL` env variable and
generate the appropriate configuration file. This setting defines the root url
of the loop server, without trailing slash.
Usage
-----
For development, run a local static file server:
$ make runserver
Then point your browser at:
- `http://localhost:3000/content/` for all public webapp contents,
- `http://localhost:3000/test/` for tests.
**Note:** the provided static file server for web contents is **not** intended
for production use.
Code linting
------------
$ make lint
License
-------
The Loop server code is released under the terms of the
[Mozilla Public License v2.0](http://www.mozilla.org/MPL/2.0/). See the
`LICENSE` file at the root of the repository.

View File

@ -0,0 +1,11 @@
/* 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/. */
body {
width: 100%;
/* prevent the video convsersation elements to occupy the whole available
width hence the height while keeping aspect ratio */
max-width: 730px;
margin: 0 auto;
}

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop</title>
<link rel="stylesheet" type="text/css" href="shared/css/common.css">
<link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
<link rel="stylesheet" type="text/css" href="css/webapp.css">
<link rel="prefetch" type="application/l10n" href="l10n/data.ini">
</head>
<body onload="loop.webapp.init();">
<header>
<h1>Loop</h1>
</header>
<div id="messages"></div>
<div id="main"></div>
<!-- libs -->
<script src="https://static.opentok.com/webrtc/v2.2/js/opentok.min.js"></script>
<script type="text/javascript" src="shared/libs/webl10n-20130617.js"></script>
<script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
<script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
<script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
<!-- 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/webapp.js"></script>
<script>
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
window.addEventListener('localized', function() {
document.documentElement.lang = document.webL10n.getLanguage();
document.documentElement.dir = document.webL10n.getDirection();
}, false);
</script>
</body>
</html>

View File

@ -0,0 +1,230 @@
/* 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 */
var loop = loop || {};
loop.webapp = (function($, _, OT) {
"use strict";
loop.config = loop.config || {};
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
baseServerUrl = loop.config.serverUrl;
/**
* App router.
* @type {loop.webapp.WebappRouter}
*/
var router;
/**
* Homepage view.
*/
var HomeView = sharedViews.BaseView.extend({
template: _.template('<p data-l10n-id="welcome"></p>')
});
/**
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
*/
var ConversationFormView = sharedViews.BaseView.extend({
template: _.template([
'<form>',
' <p>',
' <button class="btn btn-success" data-l10n-id="start_call"></button>',
' </p>',
'</form>'
].join("")),
events: {
"submit": "initiate"
},
/**
* Constructor.
*
* Required options:
* - {loop.shared.model.ConversationModel} model Conversation model.
* - {loop.shared.views.NotificationListView} notifier Notifier component.
*
* @param {Object} options Options object.
*/
initialize: function(options) {
options = options || {};
if (!options.model) {
throw new Error("missing required model");
}
this.model = options.model;
if (!options.notifier) {
throw new Error("missing required notifier");
}
this.notifier = options.notifier;
this.listenTo(this.model, "session:error", this._onSessionError);
},
_onSessionError: function(error) {
console.error(error);
this.notifier.errorL10n("unable_retrieve_call_info");
},
/**
* Disables this form to prevent multiple submissions.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=991126
*/
disableForm: function() {
this.$("button").attr("disabled", "disabled");
},
/**
* Initiates the call.
*
* @param {SubmitEvent} event
*/
initiate: function(event) {
event.preventDefault();
this.model.initiate({
baseServerUrl: baseServerUrl,
outgoing: true
});
this.disableForm();
}
});
/**
* Webapp Router.
*/
var WebappRouter = loop.shared.router.BaseConversationRouter.extend({
routes: {
"": "home",
"unsupportedDevice": "unsupportedDevice",
"unsupportedBrowser": "unsupportedBrowser",
"call/ongoing/:token": "loadConversation",
"call/:token": "initiate"
},
initialize: function() {
// Load default view
this.loadView(new HomeView());
},
/**
* @override {loop.shared.router.BaseConversationRouter.startCall}
*/
startCall: function() {
if (!this._conversation.get("loopToken")) {
this._notifier.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
trigger: true
});
}
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/
endCall: function() {
var route = "home";
if (this._conversation.get("loopToken")) {
route = "call/" + this._conversation.get("loopToken");
}
this.navigate(route, {trigger: true});
},
/**
* Default entry point.
*/
home: function() {
this.loadView(new HomeView());
},
unsupportedDevice: function() {
this.loadView(new sharedViews.UnsupportedDeviceView());
},
unsupportedBrowser: function() {
this.loadView(new sharedViews.UnsupportedBrowserView());
},
/**
* Loads conversation launcher view, setting the received conversation token
* to the current conversation model. If a session is currently established,
* terminates it first.
*
* @param {String} loopToken Loop conversation token.
*/
initiate: function(loopToken) {
// Check if a session is ongoing; if so, terminate it
if (this._conversation.get("ongoing")) {
this._conversation.endSession();
}
this._conversation.set("loopToken", loopToken);
this.loadView(new ConversationFormView({
model: this._conversation,
notifier: this._notifier
}));
},
/**
* Loads conversation establishment view.
*
*/
loadConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this.loadView(new sharedViews.ConversationView({
sdk: OT,
model: this._conversation
}));
}
});
/**
* Local helpers.
*/
function WebappHelper() {
this._iOSRegex = /^(iPad|iPhone|iPod)/;
}
WebappHelper.prototype.isIOS = function isIOS(platform) {
return this._iOSRegex.test(platform);
};
/**
* App initialization.
*/
function init() {
var helper = new WebappHelper();
router = new WebappRouter({
conversation: new sharedModels.ConversationModel({}, {sdk: OT}),
notifier: new sharedViews.NotificationListView({el: "#messages"})
});
Backbone.history.start();
if (helper.isIOS(navigator.platform)) {
router.navigate("unsupportedDevice", {trigger: true});
} else if (!OT.checkSystemRequirements()) {
router.navigate("unsupportedBrowser", {trigger: true});
}
}
return {
baseServerUrl: baseServerUrl,
ConversationFormView: ConversationFormView,
HomeView: HomeView,
WebappHelper: WebappHelper,
init: init,
WebappRouter: WebappRouter
};
})(jQuery, _, window.OT);

View File

@ -0,0 +1,31 @@
[en]
call_has_ended=Your call has ended.
missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly.
peer_ended_conversation=Your peer ended the conversation.
unable_retrieve_call_info=Unable to retrieve conversation information.
stop=Stop
start_call=Start the call
welcome=Welcome to the Loop web client.
incompatible_browser=Incompatible Browser
powered_by_webrtc=The audio and video components of Loop are powered by WebRTC.
use_latest_firefox.innerHTML=To use Loop, please use the latest version of <a href="{{ff_url}}">Firefox</a>.
incompatible_device=Incompatible device
sorry_device_unsupported=Sorry, Loop does not currently support your device.
use_firefox_windows_mac_linux=Please open this page using the latest Firefox on Windows, Android, Mac or Linux.
[fr]
call_has_ended=L'appel est terminé.
missing_conversation_info=Informations de communication manquantes.
network_disconnected=La connexion réseau semble avoir été interrompue.
peer_ended_conversation=Votre correspondant a mis fin à la communication.
unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
stop=Arrêter
start_call=Démarrer l'appel
welcome=Bienvenue sur Loop.
incompatible_browser=Navigateur non supporté
powered_by_webrtc=Les fonctionnalités audio et vidéo de Loop utilisent WebRTC.
use_latest_firefox.innerHTML=Pour utiliser Loop, merci d'utiliser la dernière version de <a href="{{ff_url}}">Firefox</a>.
incompatible_device=Plateforme non supportée
sorry_device_unsupported=Désolé, Loop ne fonctionne actuellement pas sur votre appareil.
use_firefox_windows_mac_linux=Merci d'ouvrir cette page avec une version récente de Firefox pour Windows, Android, Mac ou Linux.

View File

@ -0,0 +1,19 @@
{
"name": "Loop-client",
"description": "Video conferencing app powered by WebRTC.",
"version": "0.0.1",
"engines": {
"node": "0.10.x",
"npm":"1.3.x"
},
"dependencies": {
"express": "3.x"
},
"devDependencies": {
"jshint": "2.x"
},
"scripts": {
"test": "make test",
"start": "make runserver"
}
}

View File

@ -0,0 +1,17 @@
/* 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/. */
var express = require('express');
var app = express();
// This lets /test/ be mapped to the right place for running tests
app.use(express.static(__dirname + '/../'));
// This lets /content/ be mappy right for the static contents.
app.use(express.static(__dirname + '/'));
app.listen(3000);
console.log("Serving repository root over HTTP at http://localhost:3000/");
console.log("Static contents are available at http://localhost:3000/content/");
console.log("Tests are viewable at http://localhost:3000/test/");
console.log("Use this for development only.");

View File

@ -0,0 +1,21 @@
== Mocha unit tests ==
These unit tests use the browser build of the [Mocha test framework][1]
and the Chai Assertion Library's [BDD interface][2].
[1]: http://visionmedia.github.io/mocha/
[2]: http://chaijs.com/api/bdd/
Aim your browser at the index.html in this directory on your localhost using
a file: or HTTP URL to run the tests. Alternately, from the top-level of your
Gecko source directory, execute:
```
./mach marionette-test browser/components/loop/test/manifest.ini
```
Next steps:
* run using JS http server so the property security context for DOM elements
is used

View File

@ -0,0 +1,215 @@
/* 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 */
var expect = chai.expect;
describe("loop.conversation", function() {
"use strict";
var ConversationRouter = loop.conversation.ConversationRouter,
sandbox,
notifier;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifier = {
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
window.navigator.mozLoop = {
get serverUrl() {
return "http://example.com";
}
};
});
afterEach(function() {
delete window.navigator.mozLoop;
sandbox.restore();
});
describe("ConversationRouter", function() {
var conversation;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
sandbox.stub(conversation, "initiate");
});
describe("Routes", function() {
var router;
beforeEach(function() {
router = new ConversationRouter({
conversation: conversation,
notifier: notifier
});
sandbox.stub(router, "loadView");
});
describe("#incoming", function() {
it("should set the loopVersion on the conversation model", function() {
router.incoming("fakeVersion");
expect(conversation.get("loopVersion")).to.equal("fakeVersion");
});
it("should display the incoming call view", function() {
router.incoming("fakeVersion");
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWithExactly(router.loadView,
sinon.match.instanceOf(loop.conversation.IncomingCallView));
});
});
describe("#accept", function() {
it("should initiate the conversation", function() {
router.accept();
sinon.assert.calledOnce(conversation.initiate);
sinon.assert.calledWithExactly(conversation.initiate, {
baseServerUrl: "http://example.com",
outgoing: false
});
});
});
describe("#conversation", function() {
it("should load the ConversationView if session is set", function() {
sandbox.stub(loop.shared.views.ConversationView.prototype,
"initialize");
conversation.set("sessionId", "fakeSessionId");
router.conversation();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(loop.shared.views.ConversationView));
});
it("should not load the ConversationView if session is not set",
function() {
router.conversation();
sinon.assert.notCalled(router.loadView);
});
it("should notify the user when session is not set",
function() {
router.conversation();
sinon.assert.calledOnce(router._notifier.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n,
"cannot_start_call_session_not_ready");
});
});
describe("#ended", function() {
// XXX When the call is ended gracefully, we should check that we
// close connections nicely
it("should close the window");
});
});
describe("Events", function() {
var router, fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
sandbox.stub(loop.conversation.ConversationRouter.prototype,
"navigate");
conversation.set("loopToken", "fakeToken");
router = new loop.conversation.ConversationRouter({
conversation: conversation,
notifier: notifier
});
});
it("should navigate to call/ongoing once the call session is ready",
function() {
conversation.setReady(fakeSessionData);
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ongoing");
});
it("should navigate to call/ended when the call session ends",
function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ended");
});
it("should navigate to call/ended when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ended");
});
it("should navigate to call/{token} when network disconnects",
function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ended");
});
});
});
describe("EndedCallView", function() {
describe("#closeWindow", function() {
it("should close the conversation window", function() {
sandbox.stub(window, "close");
var view = new loop.conversation.EndedCallView();
view.closeWindow({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(window.close);
});
});
});
describe("IncomingCallView", function() {
var conversation, view;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
view = new loop.conversation.IncomingCallView({model: conversation});
});
describe("#handleAccept", function() {
it("should trigger an 'accept' conversation model event" ,
function(done) {
conversation.once("accept", function() {
done();
});
view.handleAccept({preventDefault: sandbox.spy()});
});
});
describe("#handleDecline", function() {
it("should close the window", function() {
sandbox.stub(window, "close");
view.handleDecline({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(window.close);
});
});
});
});

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop desktop-local mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
</head>
<body>
<div id="mocha">
<p><a href="../">Index</a></p>
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<!-- libs -->
<script src="../../content/libs/l10n.js"></script>
<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>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
<script src="../shared/vendor/sinon-1.9.0.js"></script>
<script>
/*global chai,mocha */
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</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/conversation.js"></script>
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
</html>

View File

@ -0,0 +1,327 @@
/* 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 */
var expect = chai.expect;
describe("loop.panel", function() {
"use strict";
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop;
function createTestRouter(fakeDocument) {
return new loop.panel.PanelRouter({
notifier: notifier,
document: fakeDocument
});
}
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);
};
notifier = {
clear: sandbox.spy(),
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
navigator.mozLoop = {
doNotDisturb: true,
get serverUrl() {
return "http://example.com";
},
getStrings: function() {
return "{}";
},
get locale() {
return "en-US";
}
};
document.mozL10n.initialize(navigator.mozLoop);
});
afterEach(function() {
delete navigator.mozLoop;
$("#fixtures").empty();
sandbox.restore();
});
describe("loop.panel.PanelRouter", function() {
describe("#constructor", function() {
it("should require a notifier", function() {
expect(function() {
new loop.panel.PanelRouter();
}).to.Throw(Error, /missing required notifier/);
});
it("should require a document", function() {
expect(function() {
new loop.panel.PanelRouter({notifier: notifier});
}).to.Throw(Error, /missing required document/);
});
});
describe("constructed", function() {
var router;
beforeEach(function() {
router = createTestRouter({
hidden: true,
addEventListener: sandbox.spy()
});
sandbox.stub(router, "loadView");
});
describe("#home", function() {
it("should reset the PanelView", function() {
sandbox.stub(router, "reset");
router.home();
sinon.assert.calledOnce(router.reset);
});
});
describe("#reset", function() {
it("should clear all pending notifications", function() {
router.reset();
sinon.assert.calledOnce(notifier.clear);
});
it("should load the home view", function() {
router.reset();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWithExactly(router.loadView,
sinon.match.instanceOf(loop.panel.PanelView));
});
});
describe("Events", function() {
it("should listen to document visibility changes", function() {
var fakeDocument = {
hidden: true,
addEventListener: sandbox.spy()
};
var router = createTestRouter(fakeDocument);
sinon.assert.calledOnce(fakeDocument.addEventListener);
sinon.assert.calledWith(fakeDocument.addEventListener,
"visibilitychange");
});
it("should trigger panel:open when the panel document is visible",
function(done) {
var router = createTestRouter({
hidden: false,
addEventListener: function(name, cb) {
setTimeout(function() {
cb({currentTarget: {hidden: false}});
}, 0);
}
});
router.once("panel:open", function() {
done();
});
});
it("should trigger panel:closed when the panel document is hidden",
function(done) {
var router = createTestRouter({
addEventListener: function(name, cb) {
hidden: true,
setTimeout(function() {
cb({currentTarget: {hidden: true}});
}, 0);
}
});
router.once("panel:closed", function() {
done();
});
});
});
});
});
describe("loop.panel.DoNotDisturbView", function() {
var view;
beforeEach(function() {
$("#fixtures").append('<div id="dnd-view"></div>');
view = new loop.panel.DoNotDisturbView({el: $("#dnd-view")});
});
describe("#toggle", function() {
beforeEach(function() {
navigator.mozLoop.doNotDisturb = false;
});
it("should toggle the value of mozLoop.doNotDisturb", function() {
view.toggle();
expect(navigator.mozLoop.doNotDisturb).eql(true);
});
it("should update the DnD checkbox value", function() {
view.toggle();
expect(view.$("input").is(":checked")).eql(true);
});
});
describe("render", function() {
it("should check the dnd checkbox when dnd is enabled", function() {
navigator.mozLoop.doNotDisturb = false;
view.render();
expect(view.$("input").is(":checked")).eql(false);
});
it("should uncheck the dnd checkbox when dnd is disabled", function() {
navigator.mozLoop.doNotDisturb = true;
view.render();
expect(view.$("input").is(":checked")).eql(true);
});
});
});
describe("loop.panel.PanelView", function() {
beforeEach(function() {
$("#fixtures").append('<div id="messages"></div><div id="main"></div>');
});
describe("#getCallUrl", function() {
it("should reset all pending notifications", function() {
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
"requestCallUrl");
var view = new loop.panel.PanelView({notifier: notifier}).render();
view.getCallUrl({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(view.notifier.clear, "clear");
});
it("should request a call url to the server", function() {
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
"requestCallUrl");
var view = new loop.panel.PanelView({notifier: notifier});
sandbox.stub(view, "getNickname").returns("foo");
view.getCallUrl({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(requestCallUrl);
sinon.assert.calledWith(requestCallUrl, "foo");
});
it("should set the call url form in a pending state", function() {
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
"requestCallUrl");
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
var view = new loop.panel.PanelView({notifier: notifier});
view.getCallUrl({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(view.setPending);
});
it("should clear the pending state when a response is received",
function() {
sandbox.stub(loop.panel.PanelView.prototype,
"clearPending");
var requestCallUrl = sandbox.stub(
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
cb("fake error");
});
var view = new loop.panel.PanelView({notifier: notifier});
view.getCallUrl({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(view.clearPending);
});
it("should notify the user when the operation failed", function() {
var requestCallUrl = sandbox.stub(
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
cb("fake error");
});
var view = new loop.panel.PanelView({notifier: notifier});
view.getCallUrl({preventDefault: sandbox.spy()});
sinon.assert.calledOnce(view.notifier.errorL10n);
sinon.assert.calledWithExactly(view.notifier.errorL10n,
"unable_retrieve_url");
});
});
describe("#onCallUrlReceived", function() {
var callUrlData;
beforeEach(function() {
callUrlData = {
call_url: "http://call.me/",
expiresAt: 1000
};
});
it("should update the text field with the call url", function() {
var view = new loop.panel.PanelView({notifier: notifier});
view.render();
view.onCallUrlReceived(callUrlData);
expect(view.$("#call-url").val()).eql("http://call.me/");
});
it("should reset all pending notifications", function() {
var view = new loop.panel.PanelView({notifier: notifier}).render();
view.onCallUrlReceived(callUrlData);
sinon.assert.calledOnce(view.notifier.clear);
});
});
describe("events", function() {
describe("goBack", function() {
it("should update the button state");
});
describe("changeButtonState", function() {
it("should do set the disabled state if there is no text");
it("should do set the enabled state if there is text");
});
});
describe("#render", function() {
it("should render a DoNotDisturbView", function() {
var renderDnD = sandbox.stub(loop.panel.DoNotDisturbView.prototype,
"render");
var view = new loop.panel.PanelView({notifier: notifier});
view.render();
sinon.assert.calledOnce(renderDnD);
});
});
});
});

View File

@ -0,0 +1,16 @@
# need to get this dir in the path so that we make the import work
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'shared'))
from frontend_tester import BaseTestFrontendUnits
class TestDesktopUnits(BaseTestFrontendUnits):
def setUp(self):
super(TestDesktopUnits, self).setUp()
self.set_server_prefix("../desktop-local/")
def test_units(self):
self.check_page("index.html")

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop test index</title>
</head>
<body>
<h1>Loop tests</h1>
<ul>
<li><a href="shared/">Shared tests</a></li>
<li><a href="desktop-local/">Local tests</a></li>
<li><a href="standalone/">Standalone tests</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,291 @@
/* 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

@ -0,0 +1,117 @@
from marionette_test import MarionetteTestCase
import threading
import SimpleHTTPServer
import SocketServer
import BaseHTTPServer
import socket
import urllib
import urlparse
import os
DEBUG = False
# XXX Once we're on a branch with bug 993478 landed, we may want to get
# rid of this HTTP server and just use the built-in one from Marionette,
# since there will less code to maintain, and it will be faster. We'll
# need to consider whether this code wants to be shared with WebDriver tests
# for other browsers, though.
#
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
BaseHTTPServer.HTTPServer):
pass
class QuietHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def log_message(self, format, *args, **kwargs):
pass
class BaseTestFrontendUnits(MarionetteTestCase):
@classmethod
def setUpClass(cls):
super(BaseTestFrontendUnits, cls).setUpClass()
if DEBUG:
handler = SimpleHTTPServer.SimpleHTTPRequestHandler
else:
handler = QuietHttpRequestHandler
# Port 0 means to select an arbitrary unused port
cls.server = ThreadingSimpleServer(('', 0), handler)
cls.ip, cls.port = cls.server.server_address
cls.server_thread = threading.Thread(target=cls.server.serve_forever)
cls.server_thread.daemon = False
cls.server_thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.server_thread.join()
# make sure everything gets GCed so it doesn't interfere with the next
# test class. Even though this is class-static, each subclass gets
# its own instance of this stuff.
cls.server_thread = None
cls.server = None
def setUp(self):
super(BaseTestFrontendUnits, self).setUp()
# This extends the timeout for find_element to 10 seconds.
# We need this as the tests take an amount of time to run after loading,
# which we have to wait for.
self.marionette.set_search_timeout(10000)
# srcdir_path should be the directory relative to this file.
def set_server_prefix(self, srcdir_path):
# We may be run from a different path than topsrcdir, e.g. in the case
# of packaged tests. If so, then we have to work out the right directory
# for the local server.
# First find the top of the working directory.
commonPath = os.path.commonprefix([__file__, os.getcwd()])
# Now get the relative path between the two
relPath = os.path.relpath(os.path.dirname(__file__), commonPath)
relPath = urllib.pathname2url(os.path.join(relPath, srcdir_path))
# Finally join the relative path with the given src path
self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port),
relPath)
def check_page(self, page):
self.marionette.navigate(urlparse.urljoin(self.server_prefix, page))
self.marionette.find_element("id", 'complete')
fail_node = self.marionette.find_element("css selector",
'.failures > em')
if fail_node.text == "0":
return
# This may want to be in a more general place triggerable by an env
# var some day if it ends up being something we need often:
#
# If you have browser-based unit tests which work when loaded manually
# but not from marionette, uncomment the two lines below to break
# on failing tests, so that the browsers won't be torn down, and you
# can use the browser debugging facilities to see what's going on.
#from ipdb import set_trace
#set_trace()
raise AssertionError(self.get_failure_details())
def get_failure_details(self):
fail_nodes = self.marionette.find_elements("css selector",
'.test.fail')
details = ["%d failure(s) encountered:" % len(fail_nodes)]
for node in fail_nodes:
details.append(
node.find_element("tag name", 'h2').text.split("\n")[0])
details.append(
node.find_element("css selector", '.error').text)
return "\n".join(details)

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop shared mocha tests</title>
<link rel="stylesheet" media="all" href="vendor/mocha-1.17.1.css">
</head>
<body>
<div id="mocha">
<p><a href="../">Index</a></p>
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<!-- libs -->
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
<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>
<!-- test dependencies -->
<script src="vendor/mocha-1.17.1.js"></script>
<script src="vendor/chai-1.9.0.js"></script>
<script src="vendor/sinon-1.9.0.js"></script>
<script>
/*global chai, mocha */
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</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>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
</html>

View File

@ -0,0 +1,268 @@
/* 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 */
var expect = chai.expect;
describe("loop.shared.models", function() {
"use strict";
var sharedModels = loop.shared.models,
sandbox, fakeXHR, requests = [], fakeSDK, fakeSession, fakeSessionData;
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);
};
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
fakeSession = _.extend({
connect: sandbox.spy(),
disconnect: sandbox.spy(),
unpublish: sandbox.spy()
}, Backbone.Events);
fakeSDK = {
initPublisher: sandbox.spy(),
initSession: sandbox.stub().returns(fakeSession)
};
});
afterEach(function() {
sandbox.restore();
});
describe("ConversationModel", function() {
describe("#initialize", function() {
it("should require a sdk option", function() {
expect(function() {
new sharedModels.ConversationModel();
}).to.Throw(Error, /missing required sdk/);
});
});
describe("constructed", function() {
var conversation, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
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");
});
describe("#initiate", function() {
it("call requestCallInfo on the client for outgoing calls",
function() {
conversation.initiate({
baseServerUrl: fakeBaseServerUrl,
outgoing: true
});
sinon.assert.calledOnce(reqCallInfoStub);
sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
});
it("should not call requestCallsInfo on the client for outgoing calls",
function() {
conversation.initiate({
baseServerUrl: fakeBaseServerUrl,
outgoing: true
});
sinon.assert.notCalled(reqCallsInfoStub);
});
it("call requestCallsInfo on the client for incoming calls",
function() {
conversation.initiate({
baseServerUrl: fakeBaseServerUrl,
outgoing: false
});
sinon.assert.calledOnce(reqCallsInfoStub);
sinon.assert.calledWith(reqCallsInfoStub);
});
it("should not call requestCallInfo on the client for incoming calls",
function() {
conversation.initiate({
baseServerUrl: fakeBaseServerUrl,
outgoing: false
});
sinon.assert.notCalled(reqCallInfoStub);
});
it("should update conversation session information from server data",
function() {
sandbox.stub(conversation, "setReady");
reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
conversation.initiate({
baseServerUrl: fakeBaseServerUrl,
outgoing: true
});
sinon.assert.calledOnce(conversation.setReady);
sinon.assert.calledWith(conversation.setReady, fakeSessionData);
});
it("should trigger a `session:error` on failure", function(done) {
reqCallInfoStub.callsArgWith(1,
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,
outgoing: true
});
});
});
describe("#setReady", function() {
it("should update conversation session information", function() {
conversation.setReady(fakeSessionData);
expect(conversation.get("sessionId")).eql("sessionId");
expect(conversation.get("sessionToken")).eql("sessionToken");
expect(conversation.get("apiKey")).eql("apiKey");
});
it("should trigger a `session:ready` event", function(done) {
conversation.on("session:ready", function() {
done();
}).setReady(fakeSessionData);
});
});
describe("#startSession", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
model.startSession();
});
it("should start a session", function() {
sinon.assert.calledOnce(fakeSDK.initSession);
});
describe("Session events", function() {
it("should trigger a session:connected event on sessionConnected",
function(done) {
model.once("session:connected", function(){ done(); });
fakeSession.trigger("sessionConnected");
});
it("should trigger a session:ended event on sessionDisconnected",
function(done) {
model.once("session:ended", function(){ done(); });
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
});
it("should set the ongoing attribute to false on sessionDisconnected",
function(done) {
model.once("session:ended", function() {
expect(model.get("ongoing")).eql(false);
done();
});
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
});
describe("connectionDestroyed event received", function() {
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
it("should trigger a session:peer-hungup model event",
function(done) {
model.once("session:peer-hungup", function(event) {
expect(event.connectionId).eql(42);
done();
});
fakeSession.trigger("connectionDestroyed", fakeEvent);
});
it("should terminate the session", function() {
sandbox.stub(model, "endSession");
fakeSession.trigger("connectionDestroyed", fakeEvent);
sinon.assert.calledOnce(model.endSession);
});
});
describe("networkDisconnected event received", function() {
it("should trigger a session:network-disconnected event",
function(done) {
model.once("session:network-disconnected", function() {
done();
});
fakeSession.trigger("networkDisconnected");
});
it("should terminate the session", function() {
sandbox.stub(model, "endSession");
fakeSession.trigger("networkDisconnected", {reason: "ko"});
sinon.assert.calledOnce(model.endSession);
});
});
});
});
describe("#endSession", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
model.startSession();
});
it("should disconnect current session", function() {
model.endSession();
sinon.assert.calledOnce(fakeSession.disconnect);
});
it("should set the ongoing attribute to false", function() {
model.endSession();
expect(model.get("ongoing")).eql(false);
});
it("should stop listening to session events once the session is " +
"actually disconnected", function() {
sandbox.stub(model, "stopListening");
model.endSession();
model.trigger("session:ended");
sinon.assert.calledOnce(model.stopListening);
});
});
});
});
});

View File

@ -0,0 +1,178 @@
/* 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 */
var expect = chai.expect;
describe("loop.shared.router", function() {
"use strict";
var sandbox, notifier;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifier = {
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
});
afterEach(function() {
sandbox.restore();
});
describe("BaseRouter", function() {
beforeEach(function() {
$("#fixtures").html('<div id="main"></div>');
});
afterEach(function() {
$("#fixtures").empty();
});
describe("#constructor", function() {
it("should require a notifier", function() {
expect(function() {
new loop.shared.router.BaseRouter();
}).to.Throw(Error, /missing required notifier/);
});
describe("inherited", function() {
var ExtendedRouter = loop.shared.router.BaseRouter.extend({});
it("should require a notifier", function() {
expect(function() {
new ExtendedRouter();
}).to.Throw(Error, /missing required notifier/);
});
});
});
describe("constructed", function() {
var router, view, TestRouter;
beforeEach(function() {
TestRouter = loop.shared.router.BaseRouter.extend({});
var TestView = loop.shared.views.BaseView.extend({
template: _.template("<p>plop</p>")
});
view = new TestView();
router = new TestRouter({notifier: notifier});
});
describe("#loadView", function() {
it("should set the active view", function() {
router.loadView(view);
expect(router._activeView).eql(view);
});
it("should load and render the passed view", function() {
router.loadView(view);
expect($("#main p").text()).eql("plop");
});
});
describe("#updateView", function() {
it("should update the main element with provided contents", function() {
router.updateView($("<p>plip</p>"));
expect($("#main p").text()).eql("plip");
});
});
});
});
describe("BaseConversationRouter", function() {
var conversation, TestRouter;
beforeEach(function() {
TestRouter = loop.shared.router.BaseConversationRouter.extend({
startCall: sandbox.spy(),
endCall: sandbox.spy()
});
conversation = new loop.shared.models.ConversationModel({
loopToken: "fakeToken"
}, {sdk: {}});
});
describe("#constructor", function() {
it("should require a ConversationModel instance", function() {
expect(function() {
new TestRouter();
}).to.Throw(Error, /missing required conversation/);
});
});
describe("Events", function() {
var router, fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
router = new TestRouter({
conversation: conversation,
notifier: notifier
});
});
it("should call startCall() once the call session is ready", function() {
conversation.trigger("session:ready");
sinon.assert.calledOnce(router.startCall);
});
it("should call endCall() when conversation ended", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.endCall);
});
it("should warn the user that the session has ended", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(notifier.warnL10n);
sinon.assert.calledWithExactly(notifier.warnL10n,
"call_has_ended");
});
it("should warn the user when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(notifier.warnL10n);
sinon.assert.calledWithExactly(notifier.warnL10n,
"peer_ended_conversation");
});
it("should call endCall() when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(router.endCall);
});
it("should warn the user when network disconnects", function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(notifier.warnL10n);
sinon.assert.calledWithExactly(notifier.warnL10n,
"network_disconnected");
});
it("should call endCall() when network disconnects", function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(router.endCall);
});
});
});
});

View File

@ -0,0 +1,22 @@
/* 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/. */
/**
* This file mocks the functions from the OT sdk that we use. This is to provide
* an interface that tests can mock out, without needing to maintain a copy of
* the sdk or load one from the network.
*/
(function (window) {
"use strict";
if (!window.OT) {
window.OT = {};
}
window.OT.checkSystemRequirements = function() {
return true;
};
})(window);

View File

@ -0,0 +1,16 @@
# need to get this dir in the path so that we make the import work
import os
import sys
sys.path.append(os.path.dirname(__file__))
from frontend_tester import BaseTestFrontendUnits
class TestSharedUnits(BaseTestFrontendUnits):
def setUp(self):
super(TestSharedUnits, self).setUp()
self.set_server_prefix(".")
def test_units(self):
self.check_page("index.html")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,270 @@
@charset "utf-8";
body {
margin:0;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul,
#mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1,
#mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: #c09853;
}
#mocha .test.pass.slow .duration {
background: #b94a48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: #fff;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
max-height: 300px;
overflow: auto;
}
/**
* (1): approximate for browsers not supporting calc
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
* ^^ seriously
*/
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
max-width: 85%; /*(1)*/
max-width: calc(100% - 42px); /*(2)*/
word-wrap: break-word;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
-moz-border-radius: 3px;
-moz-box-shadow: 0 1px 3px #eee;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: #eee;
font-size: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition: opacity 200ms;
-moz-transition: opacity 200ms;
transition: opacity 200ms;
opacity: 0.3;
color: #888;
}
#mocha .test:hover a.replay {
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: #c00;
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
}
#mocha-stats em {
color: black;
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid #eee;
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: #ddd; }
#mocha code .init { color: #2f6fad; }
#mocha code .string { color: #5890ad; }
#mocha code .keyword { color: #8a6343; }
#mocha code .number { color: #2f6fad; }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,420 @@
/* 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 */
var expect = chai.expect;
var l10n = document.webL10n || document.mozL10n;
describe("loop.shared.views", function() {
"use strict";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
});
afterEach(function() {
$("#fixtures").empty();
sandbox.restore();
});
describe("L10nView", function() {
beforeEach(function() {
sandbox.stub(l10n, "translate");
});
it("should translate generated contents on render()", function() {
var TestView = loop.shared.views.L10nView.extend();
var view = new TestView();
view.render();
sinon.assert.calledOnce(l10n.translate);
sinon.assert.calledWithExactly(l10n.translate, view.el);
});
});
describe("ConversationView", function() {
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
fakeSession = _.extend({
connection: {connectionId: 42},
connect: sandbox.spy(),
disconnect: sandbox.spy(),
publish: sandbox.spy(),
unpublish: sandbox.spy(),
subscribe: sandbox.spy()
}, Backbone.Events);
fakePublisher = {
on: sandbox.spy(),
off: sandbox.spy()
};
fakeSDK = {
initPublisher: sandbox.stub().returns(fakePublisher),
initSession: sandbox.stub().returns(fakeSession)
};
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
});
describe("#initialize", function() {
it("should require a sdk object", function() {
expect(function() {
new sharedViews.ConversationView();
}).to.Throw(Error, /sdk/);
});
it("should start a session", function() {
sandbox.stub(model, "startSession");
new sharedViews.ConversationView({sdk: fakeSDK, model: model});
sinon.assert.calledOnce(model.startSession);
});
});
describe("constructed", function() {
describe("#hangup", function() {
it("should disconnect the session", function() {
var view = new sharedViews.ConversationView({
sdk: fakeSDK,
model: model
});
sandbox.stub(model, "endSession");
view.publish();
view.hangup({preventDefault: function() {}});
sinon.assert.calledOnce(model.endSession);
});
});
describe("#publish", function() {
var view;
beforeEach(function() {
view = new sharedViews.ConversationView({
sdk: fakeSDK,
model: model
});
});
it("should publish local stream", function() {
view.publish();
sinon.assert.calledOnce(fakeSDK.initPublisher);
sinon.assert.calledOnce(fakeSession.publish);
});
it("should start listening to OT publisher accessDialogOpened and " +
" accessDenied events",
function() {
view.publish();
sinon.assert.calledTwice(fakePublisher.on);
sinon.assert.calledWith(fakePublisher.on, "accessDialogOpened");
sinon.assert.calledWith(fakePublisher.on, "accessDenied");
});
});
describe("#unpublish", function() {
var view;
beforeEach(function() {
view = new sharedViews.ConversationView({
sdk: fakeSDK,
model: model
});
view.publish();
});
it("should unpublish local stream", function() {
view.unpublish();
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unsubscribe from accessDialogOpened and accessDenied events",
function() {
view.unpublish();
sinon.assert.calledTwice(fakePublisher.off);
sinon.assert.calledWith(fakePublisher.off, "accessDialogOpened");
sinon.assert.calledWith(fakePublisher.off, "accessDenied");
});
});
describe("Model events", function() {
var view;
beforeEach(function() {
sandbox.stub(sharedViews.ConversationView.prototype, "publish");
sandbox.stub(sharedViews.ConversationView.prototype, "unpublish");
view = new sharedViews.ConversationView({sdk: fakeSDK, model: model});
});
it("should publish local stream on session:connected", function() {
model.trigger("session:connected");
sinon.assert.calledOnce(view.publish);
});
it("should publish remote streams on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
var s2 = {connection: {connectionId: 43}};
model.trigger("session:stream-created", {streams: [s1, s2]});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s2);
});
it("should unpublish local stream on session:ended", function() {
model.trigger("session:ended");
sinon.assert.calledOnce(view.unpublish);
});
it("should unpublish local stream on session:peer-hungup", function() {
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(view.unpublish);
});
it("should unpublish local stream on session:network-disconnected",
function() {
model.trigger("session:network-disconnected");
sinon.assert.calledOnce(view.unpublish);
});
});
});
});
describe("NotificationView", function() {
var collection, model, view;
beforeEach(function() {
$("#fixtures").append('<div id="test-notif"></div>');
model = new sharedModels.NotificationModel({
level: "error",
message: "plop"
});
collection = new sharedModels.NotificationCollection([model]);
view = new sharedViews.NotificationView({
el: $("#test-notif"),
collection: collection,
model: model
});
});
describe("#dismiss", function() {
it("should automatically dismiss notification after 500ms", function() {
view.render().dismiss({preventDefault: sandbox.spy()});
expect(view.$(".message").text()).eql("plop");
sandbox.clock.tick(500);
expect(collection).to.have.length.of(0);
expect($("#test-notif").html()).eql(undefined);
});
});
describe("#render", function() {
it("should render template with model attribute values", function() {
view.render();
expect(view.$(".message").text()).eql("plop");
});
});
});
describe("NotificationListView", function() {
var coll, notifData, testNotif;
beforeEach(function() {
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
notifData = {level: "error", message: "plop"};
testNotif = new sharedModels.NotificationModel(notifData);
coll = new sharedModels.NotificationCollection();
});
describe("#initialize", function() {
it("should accept a collection option", function() {
var view = new sharedViews.NotificationListView({collection: coll});
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
it("should set a default collection when none is passed", function() {
var view = new sharedViews.NotificationListView();
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
});
describe("#clear", function() {
it("should clear all notifications from the collection", function() {
var view = new sharedViews.NotificationListView();
view.notify(testNotif);
view.clear();
expect(coll).to.have.length.of(0);
});
});
describe("#notify", function() {
var view;
beforeEach(function() {
view = new sharedViews.NotificationListView({collection: coll});
});
describe("adds a new notification to the stack", function() {
it("using a plain object", function() {
view.notify(notifData);
expect(coll).to.have.length.of(1);
});
it("using a NotificationModel instance", function() {
view.notify(testNotif);
expect(coll).to.have.length.of(1);
});
});
});
describe("#notifyL10n", function() {
var view;
beforeEach(function() {
view = new sharedViews.NotificationListView({collection: coll});
});
it("should translate a message string identifier", function() {
view.notifyL10n("fakeId", "warning");
sinon.assert.calledOnce(l10n.get);
sinon.assert.calledWithExactly(l10n.get, "fakeId");
});
it("should notify end user with the provided message", function() {
sandbox.stub(view, "notify");
view.notifyL10n("fakeId", "warning");
sinon.assert.calledOnce(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "warning"
});
});
});
describe("#warn", function() {
it("should add a warning notification to the stack", function() {
var view = new sharedViews.NotificationListView({collection: coll});
view.warn("watch out");
expect(coll).to.have.length.of(1);
expect(coll.at(0).get("level")).eql("warning");
expect(coll.at(0).get("message")).eql("watch out");
});
});
describe("#warnL10n", function() {
it("should warn using a l10n string id", function() {
var view = new sharedViews.NotificationListView({collection: coll});
sandbox.stub(view, "notify");
view.warnL10n("fakeId");
sinon.assert.called(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "warning"
});
});
});
describe("#error", function() {
it("should add an error notification to the stack", function() {
var view = new sharedViews.NotificationListView({collection: coll});
view.error("wrong");
expect(coll).to.have.length.of(1);
expect(coll.at(0).get("level")).eql("error");
expect(coll.at(0).get("message")).eql("wrong");
});
});
describe("#errorL10n", function() {
it("should notify an error using a l10n string id", function() {
var view = new sharedViews.NotificationListView({collection: coll});
sandbox.stub(view, "notify");
view.errorL10n("fakeId");
sinon.assert.called(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "error"
});
});
});
describe("Collection events", function() {
var view;
beforeEach(function() {
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
view = new sharedViews.NotificationListView({collection: coll});
});
it("should render when a notification is added to the collection",
function() {
coll.add(testNotif);
sinon.assert.calledOnce(view.render);
});
it("should render when a notification is removed from the collection",
function() {
coll.add(testNotif);
coll.remove(testNotif);
sinon.assert.calledTwice(view.render);
});
it("should render when the collection is reset", function() {
coll.reset();
sinon.assert.calledOnce(view.render);
});
});
});
});

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
</head>
<body>
<div id="mocha">
<p><a href="../">Index</a></p>
<p><a href="../shared/">Shared Tests</a></p>
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<!-- libs -->
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
<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>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
<script src="../shared/vendor/sinon-1.9.0.js"></script>
<script src="../shared/sdk_mock.js"></script>
<script>
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</script>
<!-- App scripts -->
<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/webapp.js"></script>
<!-- Test scripts -->
<script src="webapp_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
# need to get this dir in the path so that we make the import work
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'shared'))
from frontend_tester import BaseTestFrontendUnits
class TestDesktopUnits(BaseTestFrontendUnits):
def setUp(self):
super(TestDesktopUnits, self).setUp()
self.set_server_prefix("../standalone/")
def test_units(self):
self.check_page("index.html")

View File

@ -0,0 +1,344 @@
/* 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 */
var expect = chai.expect;
describe("loop.webapp", function() {
"use strict";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
sandbox,
notifier;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifier = {
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy(),
};
});
afterEach(function() {
sandbox.restore();
});
describe("#init", function() {
var WebappRouter;
beforeEach(function() {
WebappRouter = loop.webapp.WebappRouter;
sandbox.stub(WebappRouter.prototype, "navigate");
});
afterEach(function() {
Backbone.history.stop();
});
it("should navigate to the unsupportedDevice route if the sdk detects " +
"the device is running iOS", function() {
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(true);
loop.webapp.init();
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
"unsupportedDevice", {trigger: true});
});
it("should navigate to the unsupportedBrowser route if the sdk detects " +
"the browser is unsupported", function() {
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(false);
sandbox.stub(window.OT, "checkSystemRequirements").returns(false);
loop.webapp.init();
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
"unsupportedBrowser", {trigger: true});
});
});
describe("WebappRouter", function() {
var router, conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
router = new loop.webapp.WebappRouter({
conversation: conversation,
notifier: notifier
});
sandbox.stub(router, "loadView");
sandbox.stub(router, "navigate");
});
describe("#startCall", function() {
it("should navigate back home if session token is missing", function() {
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
});
it("should notify the user if session token is missing", function() {
router.startCall();
sinon.assert.calledOnce(notifier.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n,
"missing_conversation_info");
});
it("should navigate to call/ongoing/:token if session token is available",
function() {
conversation.set("loopToken", "fake");
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
});
});
describe("#endCall", function() {
it("should navigate to home if session token is unset", function() {
router.endCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
});
it("should navigate to call/:token if session token is set", function() {
conversation.set("loopToken", "fake");
router.endCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fake");
});
});
describe("Routes", function() {
describe("#home", function() {
it("should load the HomeView", function() {
router.home();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(loop.webapp.HomeView));
});
});
describe("#initiate", function() {
it("should set the token on the conversation model", function() {
router.initiate("fakeToken");
expect(conversation.get("loopToken")).eql("fakeToken");
});
it("should load the ConversationFormView", function() {
router.initiate("fakeToken");
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(loop.webapp.ConversationFormView));
});
// https://bugzilla.mozilla.org/show_bug.cgi?id=991118
it("should terminate any ongoing call session", function() {
sinon.stub(conversation, "endSession");
conversation.set("ongoing", true);
router.initiate("fakeToken");
sinon.assert.calledOnce(conversation.endSession);
});
});
describe("#loadConversation", function() {
it("should load the ConversationView if session is set", function() {
sandbox.stub(sharedViews.ConversationView.prototype, "initialize");
conversation.set("sessionId", "fakeSessionId");
router.loadConversation();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(sharedViews.ConversationView));
});
it("should navigate to #call/{token} if session isn't ready",
function() {
router.loadConversation("fakeToken");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});
});
describe("#unsupportedDevice", function() {
it("should load the UnsupportedDeviceView", function() {
router.unsupportedDevice();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(sharedViews.UnsupportedDeviceView));
});
});
describe("#unsupportedBrowser", function() {
it("should load the UnsupportedBrowserView", function() {
router.unsupportedBrowser();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWith(router.loadView,
sinon.match.instanceOf(sharedViews.UnsupportedBrowserView));
});
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
conversation.set("loopToken", "fakeToken");
});
it("should navigate to call/ongoing/:token once call session is ready",
function() {
conversation.trigger("session:ready");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
});
it("should navigate to call/{token} when conversation ended", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});
it("should navigate to call/{token} when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});
it("should navigate to call/{token} when network disconnects",
function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});
});
});
describe("ConversationFormView", function() {
var conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
});
describe("#initialize", function() {
it("should require a conversation option", function() {
expect(function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
});
});
describe("#initiate", function() {
var conversation, initiate, view, fakeSubmitEvent;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier
});
fakeSubmitEvent = {preventDefault: sinon.spy()};
initiate = sinon.stub(conversation, "initiate");
});
it("should start the conversation establishment process", function() {
conversation.set("loopToken", "fake");
view.initiate(fakeSubmitEvent);
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
sinon.assert.calledOnce(initiate);
sinon.assert.calledWith(initiate, {
baseServerUrl: loop.webapp.baseServerUrl,
outgoing: true
});
});
it("should disable current form once session is initiated", function() {
sandbox.stub(view, "disableForm");
conversation.set("loopToken", "fake");
view.initiate(fakeSubmitEvent);
sinon.assert.calledOnce(view.disableForm);
});
});
describe("Events", function() {
var conversation, view;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({
loopToken: "fake"
}, {sdk: {}});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier
});
});
it("should trigger a notication when a session:error model event is " +
" received", function() {
conversation.trigger("session:error", "tech error");
sinon.assert.calledOnce(notifier.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n,
"unable_retrieve_call_info");
});
});
});
describe("WebappHelper", function() {
var helper;
beforeEach(function() {
helper = new loop.webapp.WebappHelper();
});
describe("#isIOS", function() {
it("should detect iOS", function() {
expect(helper.isIOS("iPad")).eql(true);
expect(helper.isIOS("iPod")).eql(true);
expect(helper.isIOS("iPhone")).eql(true);
expect(helper.isIOS("iPhone Simulator")).eql(true);
});
it("shouldn't detect iOS with other platforms", function() {
expect(helper.isIOS("MacIntel")).eql(false);
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More