author | Wes Kocher <wkocher@mozilla.com> |
Mon, 05 Jan 2015 17:08:49 -0800 | |
changeset 222089 | 2a193b7f395c8e6f3c21e83777ce2f540e4c04fe |
parent 222079 | fd223e4af53dd3fa313a7bc04a869a95374f689a (current diff) |
parent 222088 | 724554c093a8e1c621ad3920bdc00c9a3494066e (diff) |
child 222109 | 6056958e94946d24a5024ac53d88bdec93fbd70d |
child 222164 | 337faa3baf86d32dece801d3feb57c4649c9cb00 |
child 222228 | 77c0488fa25d712dfc3f0d1fb9161e07cf2a2693 |
push id | 28057 |
push user | kwierso@gmail.com |
push date | Tue, 06 Jan 2015 01:09:53 +0000 |
treeherder | mozilla-central@2a193b7f395c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 37.0a1 |
first release with | nightly linux32
2a193b7f395c
/
37.0a1
/
20150106030201
/
files
nightly linux64
2a193b7f395c
/
37.0a1
/
20150106030201
/
files
nightly mac
2a193b7f395c
/
37.0a1
/
20150106030201
/
files
nightly win32
2a193b7f395c
/
37.0a1
/
20150106030201
/
files
nightly win64
2a193b7f395c
/
37.0a1
/
20150106030201
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
37.0a1
/
20150106030201
/
pushlog to previous
nightly linux64
37.0a1
/
20150106030201
/
pushlog to previous
nightly mac
37.0a1
/
20150106030201
/
pushlog to previous
nightly win32
37.0a1
/
20150106030201
/
pushlog to previous
nightly win64
37.0a1
/
20150106030201
/
pushlog to previous
|
--- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -11,531 +11,21 @@ var loop = loop || {}; loop.conversation = (function(mozL10n) { "use strict"; var sharedViews = loop.shared.views; var sharedMixins = loop.shared.mixins; var sharedModels = loop.shared.models; var sharedActions = loop.shared.actions; + var IncomingConversationView = loop.conversationViews.IncomingConversationView; var OutgoingConversationView = loop.conversationViews.OutgoingConversationView; var CallIdentifierView = loop.conversationViews.CallIdentifierView; var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView; - - // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>" - var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/; - - var IncomingCallView = React.createClass({displayName: 'IncomingCallView', - mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], - - propTypes: { - model: React.PropTypes.object.isRequired, - video: React.PropTypes.bool.isRequired - }, - - getDefaultProps: function() { - return { - showMenu: false, - video: true - }; - }, - - clickHandler: function(e) { - var target = e.target; - if (!target.classList.contains('btn-chevron')) { - this._hideDeclineMenu(); - } - }, - - _handleAccept: function(callType) { - return function() { - this.props.model.set("selectedCallType", callType); - this.props.model.trigger("accept"); - }.bind(this); - }, - - _handleDecline: function() { - this.props.model.trigger("decline"); - }, - - _handleDeclineBlock: function(e) { - this.props.model.trigger("declineAndBlock"); - /* Prevent event propagation - * stop the click from reaching parent element */ - return false; - }, - - /* - * Generate props for <AcceptCallButton> component based on - * incoming call type. An incoming video call will render a video - * answer button primarily, an audio call will flip them. - **/ - _answerModeProps: function() { - var videoButton = { - handler: this._handleAccept("audio-video"), - className: "fx-embedded-btn-icon-video", - tooltip: "incoming_call_accept_audio_video_tooltip" - }; - var audioButton = { - handler: this._handleAccept("audio"), - className: "fx-embedded-btn-audio-small", - tooltip: "incoming_call_accept_audio_only_tooltip" - }; - var props = {}; - props.primary = videoButton; - props.secondary = audioButton; - - // When video is not enabled on this call, we swap the buttons around. - if (!this.props.video) { - audioButton.className = "fx-embedded-btn-icon-audio"; - videoButton.className = "fx-embedded-btn-video-small"; - props.primary = audioButton; - props.secondary = videoButton; - } - - return props; - }, - - render: function() { - /* jshint ignore:start */ - var dropdownMenuClassesDecline = React.addons.classSet({ - "native-dropdown-menu": true, - "conversation-window-dropdown": true, - "visually-hidden": !this.state.showMenu - }); - - return ( - React.DOM.div({className: "call-window"}, - CallIdentifierView({video: this.props.video, - peerIdentifier: this.props.model.getCallIdentifier(), - urlCreationDate: this.props.model.get("urlCreationDate"), - showIcons: true}), - - React.DOM.div({className: "btn-group call-action-group"}, - - React.DOM.div({className: "fx-embedded-call-button-spacer"}), - - React.DOM.div({className: "btn-chevron-menu-group"}, - React.DOM.div({className: "btn-group-chevron"}, - React.DOM.div({className: "btn-group"}, - - React.DOM.button({className: "btn btn-decline", - onClick: this._handleDecline}, - mozL10n.get("incoming_call_cancel_button") - ), - React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu}) - ), - - React.DOM.ul({className: dropdownMenuClassesDecline}, - React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, - mozL10n.get("incoming_call_cancel_and_block_button") - ) - ) - - ) - ), - - React.DOM.div({className: "fx-embedded-call-button-spacer"}), - - AcceptCallButton({mode: this._answerModeProps()}), - - React.DOM.div({className: "fx-embedded-call-button-spacer"}) - - ) - ) - ); - /* jshint ignore:end */ - } - }); - - /** - * Incoming call view accept button, renders different primary actions - * (answer with video / with audio only) based on the props received - **/ - var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton', - - propTypes: { - mode: React.PropTypes.object.isRequired, - }, - - render: function() { - var mode = this.props.mode; - return ( - /* jshint ignore:start */ - React.DOM.div({className: "btn-chevron-menu-group"}, - React.DOM.div({className: "btn-group"}, - React.DOM.button({className: "btn btn-accept", - onClick: mode.primary.handler, - title: mozL10n.get(mode.primary.tooltip)}, - React.DOM.span({className: "fx-embedded-answer-btn-text"}, - mozL10n.get("incoming_call_accept_button") - ), - React.DOM.span({className: mode.primary.className}) - ), - React.DOM.div({className: mode.secondary.className, - onClick: mode.secondary.handler, - title: mozL10n.get(mode.secondary.tooltip)} - ) - ) - ) - /* jshint ignore:end */ - ); - } - }); - - /** - * Something went wrong view. Displayed when there's a big problem. - * - * XXX Based on CallFailedView, but built specially until we flux-ify the - * incoming call views (bug 1088672). - */ - var GenericFailureView = React.createClass({displayName: 'GenericFailureView', - mixins: [sharedMixins.AudioMixin], - - propTypes: { - cancelCall: React.PropTypes.func.isRequired - }, - - componentDidMount: function() { - this.play("failure"); - }, - - render: function() { - document.title = mozL10n.get("generic_failure_title"); - - return ( - React.DOM.div({className: "call-window"}, - React.DOM.h2(null, mozL10n.get("generic_failure_title")), - - React.DOM.div({className: "btn-group call-action-group"}, - React.DOM.button({className: "btn btn-cancel", - onClick: this.props.cancelCall}, - mozL10n.get("cancel_button") - ) - ) - ) - ); - } - }); - - /** - * This view manages the incoming conversation views - from - * call initiation through to the actual conversation and call end. - * - * At the moment, it does more than that, these parts need refactoring out. - */ - var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView', - mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], - - propTypes: { - client: React.PropTypes.instanceOf(loop.Client).isRequired, - conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - sdk: React.PropTypes.object.isRequired, - conversationAppStore: React.PropTypes.instanceOf( - loop.store.ConversationAppStore).isRequired, - feedbackStore: - React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired - }, - - getInitialState: function() { - return { - callFailed: false, // XXX this should be removed when bug 1047410 lands. - callStatus: "start" - }; - }, - - componentDidMount: function() { - this.props.conversation.on("accept", this.accept, this); - this.props.conversation.on("decline", this.decline, this); - this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); - this.props.conversation.on("call:accepted", this.accepted, this); - this.props.conversation.on("change:publishedStream", this._checkConnected, this); - this.props.conversation.on("change:subscribedStream", this._checkConnected, this); - this.props.conversation.on("session:ended", this.endCall, this); - this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); - this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); - this.props.conversation.on("session:connection-error", this._notifyError, this); - - this.setupIncomingCall(); - }, - - componentDidUnmount: function() { - this.props.conversation.off(null, null, this); - }, - - render: function() { - switch (this.state.callStatus) { - case "start": { - document.title = mozL10n.get("incoming_call_title2"); - - // XXX Don't render anything initially, though this should probably - // be some sort of pending view, whilst we connect the websocket. - return null; - } - case "incoming": { - document.title = mozL10n.get("incoming_call_title2"); - - return ( - IncomingCallView({ - model: this.props.conversation, - video: this.props.conversation.hasVideoStream("incoming")} - ) - ); - } - case "connected": { - document.title = this.props.conversation.getCallIdentifier(); - - var callType = this.props.conversation.get("selectedCallType"); - - return ( - sharedViews.ConversationView({ - initiate: true, - sdk: this.props.sdk, - model: this.props.conversation, - video: {enabled: callType !== "audio"}} - ) - ); - } - case "end": { - // XXX To be handled with the "failed" view state when bug 1047410 lands - if (this.state.callFailed) { - return GenericFailureView({ - cancelCall: this.closeWindow.bind(this)} - ); - } - - document.title = mozL10n.get("conversation_has_ended"); - - this.play("terminated"); - - return ( - sharedViews.FeedbackView({ - feedbackStore: this.props.feedbackStore, - onAfterFeedbackReceived: this.closeWindow.bind(this)} - ) - ); - } - case "close": { - this.closeWindow(); - return (React.DOM.div(null)); - } - } - }, - - /** - * Notify the user that the connection was not possible - * @param {{code: number, message: string}} error - */ - _notifyError: function(error) { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - console.error(error); - this.setState({callFailed: true, callStatus: "end"}); - }, - - /** - * Peer hung up. Notifies the user and ends the call. - * - * Event properties: - * - {String} connectionId: OT session id - */ - _onPeerHungup: function() { - this.setState({callFailed: false, callStatus: "end"}); - }, - - /** - * Network disconnected. Notifies the user and ends the call. - */ - _onNetworkDisconnected: function() { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - this.setState({callFailed: true, callStatus: "end"}); - }, - - /** - * Incoming call route. - */ - setupIncomingCall: function() { - navigator.mozLoop.startAlerting(); - - // XXX This is a hack until we rework for the flux model in bug 1088672. - var callData = this.props.conversationAppStore.getStoreState().windowData; - - this.props.conversation.setIncomingSessionData(callData); - this._setupWebSocket(); - }, - - /** - * Starts the actual conversation - */ - accepted: function() { - this.setState({callStatus: "connected"}); - }, - - /** - * Moves the call to the end state - */ - endCall: function() { - navigator.mozLoop.calls.clearCallInProgress( - this.props.conversation.get("windowId")); - this.setState({callStatus: "end"}); - }, - - /** - * Used to set up the web socket connection and navigate to the - * call view if appropriate. - */ - _setupWebSocket: function() { - this._websocket = new loop.CallConnectionWebSocket({ - url: this.props.conversation.get("progressURL"), - websocketToken: this.props.conversation.get("websocketToken"), - callId: this.props.conversation.get("callId"), - }); - this._websocket.promiseConnect().then(function(progressStatus) { - this.setState({ - callStatus: progressStatus === "terminated" ? "close" : "incoming" - }); - }.bind(this), function() { - this._handleSessionError(); - return; - }.bind(this)); - - this._websocket.on("progress", this._handleWebSocketProgress, this); - }, - - /** - * Checks if the streams have been connected, and notifies the - * websocket that the media is now connected. - */ - _checkConnected: function() { - // Check we've had both local and remote streams connected before - // sending the media up message. - if (this.props.conversation.streamsConnected()) { - this._websocket.mediaUp(); - } - }, - - /** - * Used to receive websocket progress and to determine how to handle - * it if appropraite. - * If we add more cases here, then we should refactor this function. - * - * @param {Object} progressData The progress data from the websocket. - * @param {String} previousState The previous state from the websocket. - */ - _handleWebSocketProgress: function(progressData, previousState) { - // We only care about the terminated state at the moment. - if (progressData.state !== "terminated") - return; - - // XXX This would be nicer in the _abortIncomingCall function, but we need to stop - // it here for now due to server-side issues that are being fixed in bug 1088351. - // This is before the abort call to ensure that it happens before the window is - // closed. - navigator.mozLoop.stopAlerting(); - - // If we hit any of the termination reasons, and the user hasn't accepted - // then it seems reasonable to close the window/abort the incoming call. - // - // If the user has accepted the call, and something's happened, display - // the call failed view. - // - // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons - if (previousState === "init" || previousState === "alerting") { - this._abortIncomingCall(); - } else { - this.setState({callFailed: true, callStatus: "end"}); - } - - }, - - /** - * Silently aborts an incoming call - stops the alerting, and - * closes the websocket. - */ - _abortIncomingCall: function() { - this._websocket.close(); - // Having a timeout here lets the logging for the websocket complete and be - // displayed on the console if both are on. - setTimeout(this.closeWindow, 0); - }, - - /** - * Accepts an incoming call. - */ - accept: function() { - navigator.mozLoop.stopAlerting(); - this._websocket.accept(); - this.props.conversation.accepted(); - }, - - /** - * Declines a call and handles closing of the window. - */ - _declineCall: function() { - this._websocket.decline(); - navigator.mozLoop.calls.clearCallInProgress( - this.props.conversation.get("windowId")); - this._websocket.close(); - // Having a timeout here lets the logging for the websocket complete and be - // displayed on the console if both are on. - setTimeout(this.closeWindow, 0); - }, - - /** - * Declines an incoming call. - */ - decline: function() { - navigator.mozLoop.stopAlerting(); - this._declineCall(); - }, - - /** - * Decline and block an incoming call - * @note: - * - loopToken is the callUrl identifier. It gets set in the panel - * after a callUrl is received - */ - declineAndBlock: function() { - navigator.mozLoop.stopAlerting(); - var token = this.props.conversation.get("callToken"); - var callerId = this.props.conversation.get("callerId"); - - // If this is a direct call, we'll need to block the caller directly. - if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) { - navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) { - // XXX The conversation window will be closed when this cb is triggered - // figure out if there is a better way to report the error to the user - // (bug 1103150). - console.log(err.fileName + ":" + err.lineNumber + ": " + err.message); - }); - } else { - this.props.client.deleteCallUrl(token, - this.props.conversation.get("sessionType"), - function(error) { - // XXX The conversation window will be closed when this cb is triggered - // figure out if there is a better way to report the error to the user - // (bug 1048909). - console.log(error); - }); - } - - this._declineCall(); - }, - - /** - * Handles a error starting the session - */ - _handleSessionError: function() { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - console.error("Failed initiating the call session."); - }, - }); + var GenericFailureView = loop.conversationViews.GenericFailureView; /** * Master controller view for handling if incoming or outgoing calls are * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({displayName: 'AppControllerView', mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], @@ -706,16 +196,13 @@ loop.conversation = (function(mozL10n) { dispatcher.dispatch(new sharedActions.GetWindowData({ windowId: windowId })); } return { AppControllerView: AppControllerView, - IncomingConversationView: IncomingConversationView, - IncomingCallView: IncomingCallView, - GenericFailureView: GenericFailureView, init: init }; })(document.mozL10n); document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -11,531 +11,21 @@ var loop = loop || {}; loop.conversation = (function(mozL10n) { "use strict"; var sharedViews = loop.shared.views; var sharedMixins = loop.shared.mixins; var sharedModels = loop.shared.models; var sharedActions = loop.shared.actions; + var IncomingConversationView = loop.conversationViews.IncomingConversationView; var OutgoingConversationView = loop.conversationViews.OutgoingConversationView; var CallIdentifierView = loop.conversationViews.CallIdentifierView; var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView; - - // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>" - var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/; - - var IncomingCallView = React.createClass({ - mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], - - propTypes: { - model: React.PropTypes.object.isRequired, - video: React.PropTypes.bool.isRequired - }, - - getDefaultProps: function() { - return { - showMenu: false, - video: true - }; - }, - - clickHandler: function(e) { - var target = e.target; - if (!target.classList.contains('btn-chevron')) { - this._hideDeclineMenu(); - } - }, - - _handleAccept: function(callType) { - return function() { - this.props.model.set("selectedCallType", callType); - this.props.model.trigger("accept"); - }.bind(this); - }, - - _handleDecline: function() { - this.props.model.trigger("decline"); - }, - - _handleDeclineBlock: function(e) { - this.props.model.trigger("declineAndBlock"); - /* Prevent event propagation - * stop the click from reaching parent element */ - return false; - }, - - /* - * Generate props for <AcceptCallButton> component based on - * incoming call type. An incoming video call will render a video - * answer button primarily, an audio call will flip them. - **/ - _answerModeProps: function() { - var videoButton = { - handler: this._handleAccept("audio-video"), - className: "fx-embedded-btn-icon-video", - tooltip: "incoming_call_accept_audio_video_tooltip" - }; - var audioButton = { - handler: this._handleAccept("audio"), - className: "fx-embedded-btn-audio-small", - tooltip: "incoming_call_accept_audio_only_tooltip" - }; - var props = {}; - props.primary = videoButton; - props.secondary = audioButton; - - // When video is not enabled on this call, we swap the buttons around. - if (!this.props.video) { - audioButton.className = "fx-embedded-btn-icon-audio"; - videoButton.className = "fx-embedded-btn-video-small"; - props.primary = audioButton; - props.secondary = videoButton; - } - - return props; - }, - - render: function() { - /* jshint ignore:start */ - var dropdownMenuClassesDecline = React.addons.classSet({ - "native-dropdown-menu": true, - "conversation-window-dropdown": true, - "visually-hidden": !this.state.showMenu - }); - - return ( - <div className="call-window"> - <CallIdentifierView video={this.props.video} - peerIdentifier={this.props.model.getCallIdentifier()} - urlCreationDate={this.props.model.get("urlCreationDate")} - showIcons={true} /> - - <div className="btn-group call-action-group"> - - <div className="fx-embedded-call-button-spacer"></div> - - <div className="btn-chevron-menu-group"> - <div className="btn-group-chevron"> - <div className="btn-group"> - - <button className="btn btn-decline" - onClick={this._handleDecline}> - {mozL10n.get("incoming_call_cancel_button")} - </button> - <div className="btn-chevron" onClick={this.toggleDropdownMenu} /> - </div> - - <ul className={dropdownMenuClassesDecline}> - <li className="btn-block" onClick={this._handleDeclineBlock}> - {mozL10n.get("incoming_call_cancel_and_block_button")} - </li> - </ul> - - </div> - </div> - - <div className="fx-embedded-call-button-spacer"></div> - - <AcceptCallButton mode={this._answerModeProps()} /> - - <div className="fx-embedded-call-button-spacer"></div> - - </div> - </div> - ); - /* jshint ignore:end */ - } - }); - - /** - * Incoming call view accept button, renders different primary actions - * (answer with video / with audio only) based on the props received - **/ - var AcceptCallButton = React.createClass({ - - propTypes: { - mode: React.PropTypes.object.isRequired, - }, - - render: function() { - var mode = this.props.mode; - return ( - /* jshint ignore:start */ - <div className="btn-chevron-menu-group"> - <div className="btn-group"> - <button className="btn btn-accept" - onClick={mode.primary.handler} - title={mozL10n.get(mode.primary.tooltip)}> - <span className="fx-embedded-answer-btn-text"> - {mozL10n.get("incoming_call_accept_button")} - </span> - <span className={mode.primary.className}></span> - </button> - <div className={mode.secondary.className} - onClick={mode.secondary.handler} - title={mozL10n.get(mode.secondary.tooltip)}> - </div> - </div> - </div> - /* jshint ignore:end */ - ); - } - }); - - /** - * Something went wrong view. Displayed when there's a big problem. - * - * XXX Based on CallFailedView, but built specially until we flux-ify the - * incoming call views (bug 1088672). - */ - var GenericFailureView = React.createClass({ - mixins: [sharedMixins.AudioMixin], - - propTypes: { - cancelCall: React.PropTypes.func.isRequired - }, - - componentDidMount: function() { - this.play("failure"); - }, - - render: function() { - document.title = mozL10n.get("generic_failure_title"); - - return ( - <div className="call-window"> - <h2>{mozL10n.get("generic_failure_title")}</h2> - - <div className="btn-group call-action-group"> - <button className="btn btn-cancel" - onClick={this.props.cancelCall}> - {mozL10n.get("cancel_button")} - </button> - </div> - </div> - ); - } - }); - - /** - * This view manages the incoming conversation views - from - * call initiation through to the actual conversation and call end. - * - * At the moment, it does more than that, these parts need refactoring out. - */ - var IncomingConversationView = React.createClass({ - mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], - - propTypes: { - client: React.PropTypes.instanceOf(loop.Client).isRequired, - conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - sdk: React.PropTypes.object.isRequired, - conversationAppStore: React.PropTypes.instanceOf( - loop.store.ConversationAppStore).isRequired, - feedbackStore: - React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired - }, - - getInitialState: function() { - return { - callFailed: false, // XXX this should be removed when bug 1047410 lands. - callStatus: "start" - }; - }, - - componentDidMount: function() { - this.props.conversation.on("accept", this.accept, this); - this.props.conversation.on("decline", this.decline, this); - this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); - this.props.conversation.on("call:accepted", this.accepted, this); - this.props.conversation.on("change:publishedStream", this._checkConnected, this); - this.props.conversation.on("change:subscribedStream", this._checkConnected, this); - this.props.conversation.on("session:ended", this.endCall, this); - this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); - this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); - this.props.conversation.on("session:connection-error", this._notifyError, this); - - this.setupIncomingCall(); - }, - - componentDidUnmount: function() { - this.props.conversation.off(null, null, this); - }, - - render: function() { - switch (this.state.callStatus) { - case "start": { - document.title = mozL10n.get("incoming_call_title2"); - - // XXX Don't render anything initially, though this should probably - // be some sort of pending view, whilst we connect the websocket. - return null; - } - case "incoming": { - document.title = mozL10n.get("incoming_call_title2"); - - return ( - <IncomingCallView - model={this.props.conversation} - video={this.props.conversation.hasVideoStream("incoming")} - /> - ); - } - case "connected": { - document.title = this.props.conversation.getCallIdentifier(); - - var callType = this.props.conversation.get("selectedCallType"); - - return ( - <sharedViews.ConversationView - initiate={true} - sdk={this.props.sdk} - model={this.props.conversation} - video={{enabled: callType !== "audio"}} - /> - ); - } - case "end": { - // XXX To be handled with the "failed" view state when bug 1047410 lands - if (this.state.callFailed) { - return <GenericFailureView - cancelCall={this.closeWindow.bind(this)} - />; - } - - document.title = mozL10n.get("conversation_has_ended"); - - this.play("terminated"); - - return ( - <sharedViews.FeedbackView - feedbackStore={this.props.feedbackStore} - onAfterFeedbackReceived={this.closeWindow.bind(this)} - /> - ); - } - case "close": { - this.closeWindow(); - return (<div/>); - } - } - }, - - /** - * Notify the user that the connection was not possible - * @param {{code: number, message: string}} error - */ - _notifyError: function(error) { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - console.error(error); - this.setState({callFailed: true, callStatus: "end"}); - }, - - /** - * Peer hung up. Notifies the user and ends the call. - * - * Event properties: - * - {String} connectionId: OT session id - */ - _onPeerHungup: function() { - this.setState({callFailed: false, callStatus: "end"}); - }, - - /** - * Network disconnected. Notifies the user and ends the call. - */ - _onNetworkDisconnected: function() { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - this.setState({callFailed: true, callStatus: "end"}); - }, - - /** - * Incoming call route. - */ - setupIncomingCall: function() { - navigator.mozLoop.startAlerting(); - - // XXX This is a hack until we rework for the flux model in bug 1088672. - var callData = this.props.conversationAppStore.getStoreState().windowData; - - this.props.conversation.setIncomingSessionData(callData); - this._setupWebSocket(); - }, - - /** - * Starts the actual conversation - */ - accepted: function() { - this.setState({callStatus: "connected"}); - }, - - /** - * Moves the call to the end state - */ - endCall: function() { - navigator.mozLoop.calls.clearCallInProgress( - this.props.conversation.get("windowId")); - this.setState({callStatus: "end"}); - }, - - /** - * Used to set up the web socket connection and navigate to the - * call view if appropriate. - */ - _setupWebSocket: function() { - this._websocket = new loop.CallConnectionWebSocket({ - url: this.props.conversation.get("progressURL"), - websocketToken: this.props.conversation.get("websocketToken"), - callId: this.props.conversation.get("callId"), - }); - this._websocket.promiseConnect().then(function(progressStatus) { - this.setState({ - callStatus: progressStatus === "terminated" ? "close" : "incoming" - }); - }.bind(this), function() { - this._handleSessionError(); - return; - }.bind(this)); - - this._websocket.on("progress", this._handleWebSocketProgress, this); - }, - - /** - * Checks if the streams have been connected, and notifies the - * websocket that the media is now connected. - */ - _checkConnected: function() { - // Check we've had both local and remote streams connected before - // sending the media up message. - if (this.props.conversation.streamsConnected()) { - this._websocket.mediaUp(); - } - }, - - /** - * Used to receive websocket progress and to determine how to handle - * it if appropraite. - * If we add more cases here, then we should refactor this function. - * - * @param {Object} progressData The progress data from the websocket. - * @param {String} previousState The previous state from the websocket. - */ - _handleWebSocketProgress: function(progressData, previousState) { - // We only care about the terminated state at the moment. - if (progressData.state !== "terminated") - return; - - // XXX This would be nicer in the _abortIncomingCall function, but we need to stop - // it here for now due to server-side issues that are being fixed in bug 1088351. - // This is before the abort call to ensure that it happens before the window is - // closed. - navigator.mozLoop.stopAlerting(); - - // If we hit any of the termination reasons, and the user hasn't accepted - // then it seems reasonable to close the window/abort the incoming call. - // - // If the user has accepted the call, and something's happened, display - // the call failed view. - // - // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons - if (previousState === "init" || previousState === "alerting") { - this._abortIncomingCall(); - } else { - this.setState({callFailed: true, callStatus: "end"}); - } - - }, - - /** - * Silently aborts an incoming call - stops the alerting, and - * closes the websocket. - */ - _abortIncomingCall: function() { - this._websocket.close(); - // Having a timeout here lets the logging for the websocket complete and be - // displayed on the console if both are on. - setTimeout(this.closeWindow, 0); - }, - - /** - * Accepts an incoming call. - */ - accept: function() { - navigator.mozLoop.stopAlerting(); - this._websocket.accept(); - this.props.conversation.accepted(); - }, - - /** - * Declines a call and handles closing of the window. - */ - _declineCall: function() { - this._websocket.decline(); - navigator.mozLoop.calls.clearCallInProgress( - this.props.conversation.get("windowId")); - this._websocket.close(); - // Having a timeout here lets the logging for the websocket complete and be - // displayed on the console if both are on. - setTimeout(this.closeWindow, 0); - }, - - /** - * Declines an incoming call. - */ - decline: function() { - navigator.mozLoop.stopAlerting(); - this._declineCall(); - }, - - /** - * Decline and block an incoming call - * @note: - * - loopToken is the callUrl identifier. It gets set in the panel - * after a callUrl is received - */ - declineAndBlock: function() { - navigator.mozLoop.stopAlerting(); - var token = this.props.conversation.get("callToken"); - var callerId = this.props.conversation.get("callerId"); - - // If this is a direct call, we'll need to block the caller directly. - if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) { - navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) { - // XXX The conversation window will be closed when this cb is triggered - // figure out if there is a better way to report the error to the user - // (bug 1103150). - console.log(err.fileName + ":" + err.lineNumber + ": " + err.message); - }); - } else { - this.props.client.deleteCallUrl(token, - this.props.conversation.get("sessionType"), - function(error) { - // XXX The conversation window will be closed when this cb is triggered - // figure out if there is a better way to report the error to the user - // (bug 1048909). - console.log(error); - }); - } - - this._declineCall(); - }, - - /** - * Handles a error starting the session - */ - _handleSessionError: function() { - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - console.error("Failed initiating the call session."); - }, - }); + var GenericFailureView = loop.conversationViews.GenericFailureView; /** * Master controller view for handling if incoming or outgoing calls are * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({ mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], @@ -706,16 +196,13 @@ loop.conversation = (function(mozL10n) { dispatcher.dispatch(new sharedActions.GetWindowData({ windowId: windowId })); } return { AppControllerView: AppControllerView, - IncomingConversationView: IncomingConversationView, - IncomingCallView: IncomingCallView, - GenericFailureView: GenericFailureView, init: init }; })(document.mozL10n); document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -10,16 +10,17 @@ var loop = loop || {}; loop.conversationViews = (function(mozL10n) { var CALL_STATES = loop.store.CALL_STATES; var CALL_TYPES = loop.shared.utils.CALL_TYPES; var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; var sharedMixins = loop.shared.mixins; + var sharedModels = loop.shared.models; // This duplicates a similar function in contacts.jsx that isn't used in the // conversation window. If we get too many of these, we might want to consider // finding a logical place for them to be shared. function _getPreferredEmail(contact) { // A contact may not contain email addresses, but only a phone number. if (!contact.email || contact.email.length === 0) { return { value: "" }; @@ -124,16 +125,528 @@ loop.conversationViews = (function(mozL1 peerIdentifier: contactName, showIcons: false}), React.DOM.div(null, this.props.children) ) ); } }); + // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>" + var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/; + + var IncomingCallView = React.createClass({displayName: 'IncomingCallView', + mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], + + propTypes: { + model: React.PropTypes.object.isRequired, + video: React.PropTypes.bool.isRequired + }, + + getDefaultProps: function() { + return { + showMenu: false, + video: true + }; + }, + + clickHandler: function(e) { + var target = e.target; + if (!target.classList.contains('btn-chevron')) { + this._hideDeclineMenu(); + } + }, + + _handleAccept: function(callType) { + return function() { + this.props.model.set("selectedCallType", callType); + this.props.model.trigger("accept"); + }.bind(this); + }, + + _handleDecline: function() { + this.props.model.trigger("decline"); + }, + + _handleDeclineBlock: function(e) { + this.props.model.trigger("declineAndBlock"); + /* Prevent event propagation + * stop the click from reaching parent element */ + return false; + }, + + /* + * Generate props for <AcceptCallButton> component based on + * incoming call type. An incoming video call will render a video + * answer button primarily, an audio call will flip them. + **/ + _answerModeProps: function() { + var videoButton = { + handler: this._handleAccept("audio-video"), + className: "fx-embedded-btn-icon-video", + tooltip: "incoming_call_accept_audio_video_tooltip" + }; + var audioButton = { + handler: this._handleAccept("audio"), + className: "fx-embedded-btn-audio-small", + tooltip: "incoming_call_accept_audio_only_tooltip" + }; + var props = {}; + props.primary = videoButton; + props.secondary = audioButton; + + // When video is not enabled on this call, we swap the buttons around. + if (!this.props.video) { + audioButton.className = "fx-embedded-btn-icon-audio"; + videoButton.className = "fx-embedded-btn-video-small"; + props.primary = audioButton; + props.secondary = videoButton; + } + + return props; + }, + + render: function() { + /* jshint ignore:start */ + var dropdownMenuClassesDecline = React.addons.classSet({ + "native-dropdown-menu": true, + "conversation-window-dropdown": true, + "visually-hidden": !this.state.showMenu + }); + + return ( + React.DOM.div({className: "call-window"}, + CallIdentifierView({video: this.props.video, + peerIdentifier: this.props.model.getCallIdentifier(), + urlCreationDate: this.props.model.get("urlCreationDate"), + showIcons: true}), + + React.DOM.div({className: "btn-group call-action-group"}, + + React.DOM.div({className: "fx-embedded-call-button-spacer"}), + + React.DOM.div({className: "btn-chevron-menu-group"}, + React.DOM.div({className: "btn-group-chevron"}, + React.DOM.div({className: "btn-group"}, + + React.DOM.button({className: "btn btn-decline", + onClick: this._handleDecline}, + mozL10n.get("incoming_call_cancel_button") + ), + React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu}) + ), + + React.DOM.ul({className: dropdownMenuClassesDecline}, + React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, + mozL10n.get("incoming_call_cancel_and_block_button") + ) + ) + + ) + ), + + React.DOM.div({className: "fx-embedded-call-button-spacer"}), + + AcceptCallButton({mode: this._answerModeProps()}), + + React.DOM.div({className: "fx-embedded-call-button-spacer"}) + + ) + ) + ); + /* jshint ignore:end */ + } + }); + + /** + * Incoming call view accept button, renders different primary actions + * (answer with video / with audio only) based on the props received + **/ + var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton', + + propTypes: { + mode: React.PropTypes.object.isRequired, + }, + + render: function() { + var mode = this.props.mode; + return ( + /* jshint ignore:start */ + React.DOM.div({className: "btn-chevron-menu-group"}, + React.DOM.div({className: "btn-group"}, + React.DOM.button({className: "btn btn-accept", + onClick: mode.primary.handler, + title: mozL10n.get(mode.primary.tooltip)}, + React.DOM.span({className: "fx-embedded-answer-btn-text"}, + mozL10n.get("incoming_call_accept_button") + ), + React.DOM.span({className: mode.primary.className}) + ), + React.DOM.div({className: mode.secondary.className, + onClick: mode.secondary.handler, + title: mozL10n.get(mode.secondary.tooltip)} + ) + ) + ) + /* jshint ignore:end */ + ); + } + }); + + /** + * Something went wrong view. Displayed when there's a big problem. + * + * XXX Based on CallFailedView, but built specially until we flux-ify the + * incoming call views (bug 1088672). + */ + var GenericFailureView = React.createClass({displayName: 'GenericFailureView', + mixins: [sharedMixins.AudioMixin], + + propTypes: { + cancelCall: React.PropTypes.func.isRequired + }, + + componentDidMount: function() { + this.play("failure"); + }, + + render: function() { + document.title = mozL10n.get("generic_failure_title"); + + return ( + React.DOM.div({className: "call-window"}, + React.DOM.h2(null, mozL10n.get("generic_failure_title")), + + React.DOM.div({className: "btn-group call-action-group"}, + React.DOM.button({className: "btn btn-cancel", + onClick: this.props.cancelCall}, + mozL10n.get("cancel_button") + ) + ) + ) + ); + } + }); + + /** + * This view manages the incoming conversation views - from + * call initiation through to the actual conversation and call end. + * + * At the moment, it does more than that, these parts need refactoring out. + */ + var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView', + mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], + + propTypes: { + client: React.PropTypes.instanceOf(loop.Client).isRequired, + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + conversationAppStore: React.PropTypes.instanceOf( + loop.store.ConversationAppStore).isRequired, + feedbackStore: + React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired + }, + + getInitialState: function() { + return { + callFailed: false, // XXX this should be removed when bug 1047410 lands. + callStatus: "start" + }; + }, + + componentDidMount: function() { + this.props.conversation.on("accept", this.accept, this); + this.props.conversation.on("decline", this.decline, this); + this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); + this.props.conversation.on("call:accepted", this.accepted, this); + this.props.conversation.on("change:publishedStream", this._checkConnected, this); + this.props.conversation.on("change:subscribedStream", this._checkConnected, this); + this.props.conversation.on("session:ended", this.endCall, this); + this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); + this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); + this.props.conversation.on("session:connection-error", this._notifyError, this); + + this.setupIncomingCall(); + }, + + componentDidUnmount: function() { + this.props.conversation.off(null, null, this); + }, + + render: function() { + switch (this.state.callStatus) { + case "start": { + document.title = mozL10n.get("incoming_call_title2"); + + // XXX Don't render anything initially, though this should probably + // be some sort of pending view, whilst we connect the websocket. + return null; + } + case "incoming": { + document.title = mozL10n.get("incoming_call_title2"); + + return ( + IncomingCallView({ + model: this.props.conversation, + video: this.props.conversation.hasVideoStream("incoming")} + ) + ); + } + case "connected": { + document.title = this.props.conversation.getCallIdentifier(); + + var callType = this.props.conversation.get("selectedCallType"); + + return ( + sharedViews.ConversationView({ + initiate: true, + sdk: this.props.sdk, + model: this.props.conversation, + video: {enabled: callType !== "audio"}} + ) + ); + } + case "end": { + // XXX To be handled with the "failed" view state when bug 1047410 lands + if (this.state.callFailed) { + return GenericFailureView({ + cancelCall: this.closeWindow.bind(this)} + ); + } + + document.title = mozL10n.get("conversation_has_ended"); + + this.play("terminated"); + + return ( + sharedViews.FeedbackView({ + feedbackStore: this.props.feedbackStore, + onAfterFeedbackReceived: this.closeWindow.bind(this)} + ) + ); + } + case "close": { + this.closeWindow(); + return (React.DOM.div(null)); + } + } + }, + + /** + * Notify the user that the connection was not possible + * @param {{code: number, message: string}} error + */ + _notifyError: function(error) { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + console.error(error); + this.setState({callFailed: true, callStatus: "end"}); + }, + + /** + * Peer hung up. Notifies the user and ends the call. + * + * Event properties: + * - {String} connectionId: OT session id + */ + _onPeerHungup: function() { + this.setState({callFailed: false, callStatus: "end"}); + }, + + /** + * Network disconnected. Notifies the user and ends the call. + */ + _onNetworkDisconnected: function() { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + this.setState({callFailed: true, callStatus: "end"}); + }, + + /** + * Incoming call route. + */ + setupIncomingCall: function() { + navigator.mozLoop.startAlerting(); + + // XXX This is a hack until we rework for the flux model in bug 1088672. + var callData = this.props.conversationAppStore.getStoreState().windowData; + + this.props.conversation.setIncomingSessionData(callData); + this._setupWebSocket(); + }, + + /** + * Starts the actual conversation + */ + accepted: function() { + this.setState({callStatus: "connected"}); + }, + + /** + * Moves the call to the end state + */ + endCall: function() { + navigator.mozLoop.calls.clearCallInProgress( + this.props.conversation.get("windowId")); + this.setState({callStatus: "end"}); + }, + + /** + * Used to set up the web socket connection and navigate to the + * call view if appropriate. + */ + _setupWebSocket: function() { + this._websocket = new loop.CallConnectionWebSocket({ + url: this.props.conversation.get("progressURL"), + websocketToken: this.props.conversation.get("websocketToken"), + callId: this.props.conversation.get("callId"), + }); + this._websocket.promiseConnect().then(function(progressStatus) { + this.setState({ + callStatus: progressStatus === "terminated" ? "close" : "incoming" + }); + }.bind(this), function() { + this._handleSessionError(); + return; + }.bind(this)); + + this._websocket.on("progress", this._handleWebSocketProgress, this); + }, + + /** + * Checks if the streams have been connected, and notifies the + * websocket that the media is now connected. + */ + _checkConnected: function() { + // Check we've had both local and remote streams connected before + // sending the media up message. + if (this.props.conversation.streamsConnected()) { + this._websocket.mediaUp(); + } + }, + + /** + * Used to receive websocket progress and to determine how to handle + * it if appropraite. + * If we add more cases here, then we should refactor this function. + * + * @param {Object} progressData The progress data from the websocket. + * @param {String} previousState The previous state from the websocket. + */ + _handleWebSocketProgress: function(progressData, previousState) { + // We only care about the terminated state at the moment. + if (progressData.state !== "terminated") + return; + + // XXX This would be nicer in the _abortIncomingCall function, but we need to stop + // it here for now due to server-side issues that are being fixed in bug 1088351. + // This is before the abort call to ensure that it happens before the window is + // closed. + navigator.mozLoop.stopAlerting(); + + // If we hit any of the termination reasons, and the user hasn't accepted + // then it seems reasonable to close the window/abort the incoming call. + // + // If the user has accepted the call, and something's happened, display + // the call failed view. + // + // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons + if (previousState === "init" || previousState === "alerting") { + this._abortIncomingCall(); + } else { + this.setState({callFailed: true, callStatus: "end"}); + } + + }, + + /** + * Silently aborts an incoming call - stops the alerting, and + * closes the websocket. + */ + _abortIncomingCall: function() { + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + /** + * Accepts an incoming call. + */ + accept: function() { + navigator.mozLoop.stopAlerting(); + this._websocket.accept(); + this.props.conversation.accepted(); + }, + + /** + * Declines a call and handles closing of the window. + */ + _declineCall: function() { + this._websocket.decline(); + navigator.mozLoop.calls.clearCallInProgress( + this.props.conversation.get("windowId")); + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + /** + * Declines an incoming call. + */ + decline: function() { + navigator.mozLoop.stopAlerting(); + this._declineCall(); + }, + + /** + * Decline and block an incoming call + * @note: + * - loopToken is the callUrl identifier. It gets set in the panel + * after a callUrl is received + */ + declineAndBlock: function() { + navigator.mozLoop.stopAlerting(); + var token = this.props.conversation.get("callToken"); + var callerId = this.props.conversation.get("callerId"); + + // If this is a direct call, we'll need to block the caller directly. + if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) { + navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + // (bug 1103150). + console.log(err.fileName + ":" + err.lineNumber + ": " + err.message); + }); + } else { + this.props.client.deleteCallUrl(token, + this.props.conversation.get("sessionType"), + function(error) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + // (bug 1048909). + console.log(error); + }); + } + + this._declineCall(); + }, + + /** + * Handles a error starting the session + */ + _handleSessionError: function() { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + console.error("Failed initiating the call session."); + }, + }); + /** * View for pending conversations. Displays a cancel button and appropriate * pending/ringing strings. */ var PendingConversationView = React.createClass({displayName: 'PendingConversationView', mixins: [sharedMixins.AudioMixin], propTypes: { @@ -530,13 +1043,16 @@ loop.conversationViews = (function(mozL1 }, }); return { PendingConversationView: PendingConversationView, CallIdentifierView: CallIdentifierView, ConversationDetailView: ConversationDetailView, CallFailedView: CallFailedView, + GenericFailureView: GenericFailureView, + IncomingCallView: IncomingCallView, + IncomingConversationView: IncomingConversationView, OngoingConversationView: OngoingConversationView, OutgoingConversationView: OutgoingConversationView }; })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -10,16 +10,17 @@ var loop = loop || {}; loop.conversationViews = (function(mozL10n) { var CALL_STATES = loop.store.CALL_STATES; var CALL_TYPES = loop.shared.utils.CALL_TYPES; var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; var sharedMixins = loop.shared.mixins; + var sharedModels = loop.shared.models; // This duplicates a similar function in contacts.jsx that isn't used in the // conversation window. If we get too many of these, we might want to consider // finding a logical place for them to be shared. function _getPreferredEmail(contact) { // A contact may not contain email addresses, but only a phone number. if (!contact.email || contact.email.length === 0) { return { value: "" }; @@ -124,16 +125,528 @@ loop.conversationViews = (function(mozL1 peerIdentifier={contactName} showIcons={false} /> <div>{this.props.children}</div> </div> ); } }); + // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>" + var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/; + + var IncomingCallView = React.createClass({ + mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], + + propTypes: { + model: React.PropTypes.object.isRequired, + video: React.PropTypes.bool.isRequired + }, + + getDefaultProps: function() { + return { + showMenu: false, + video: true + }; + }, + + clickHandler: function(e) { + var target = e.target; + if (!target.classList.contains('btn-chevron')) { + this._hideDeclineMenu(); + } + }, + + _handleAccept: function(callType) { + return function() { + this.props.model.set("selectedCallType", callType); + this.props.model.trigger("accept"); + }.bind(this); + }, + + _handleDecline: function() { + this.props.model.trigger("decline"); + }, + + _handleDeclineBlock: function(e) { + this.props.model.trigger("declineAndBlock"); + /* Prevent event propagation + * stop the click from reaching parent element */ + return false; + }, + + /* + * Generate props for <AcceptCallButton> component based on + * incoming call type. An incoming video call will render a video + * answer button primarily, an audio call will flip them. + **/ + _answerModeProps: function() { + var videoButton = { + handler: this._handleAccept("audio-video"), + className: "fx-embedded-btn-icon-video", + tooltip: "incoming_call_accept_audio_video_tooltip" + }; + var audioButton = { + handler: this._handleAccept("audio"), + className: "fx-embedded-btn-audio-small", + tooltip: "incoming_call_accept_audio_only_tooltip" + }; + var props = {}; + props.primary = videoButton; + props.secondary = audioButton; + + // When video is not enabled on this call, we swap the buttons around. + if (!this.props.video) { + audioButton.className = "fx-embedded-btn-icon-audio"; + videoButton.className = "fx-embedded-btn-video-small"; + props.primary = audioButton; + props.secondary = videoButton; + } + + return props; + }, + + render: function() { + /* jshint ignore:start */ + var dropdownMenuClassesDecline = React.addons.classSet({ + "native-dropdown-menu": true, + "conversation-window-dropdown": true, + "visually-hidden": !this.state.showMenu + }); + + return ( + <div className="call-window"> + <CallIdentifierView video={this.props.video} + peerIdentifier={this.props.model.getCallIdentifier()} + urlCreationDate={this.props.model.get("urlCreationDate")} + showIcons={true} /> + + <div className="btn-group call-action-group"> + + <div className="fx-embedded-call-button-spacer"></div> + + <div className="btn-chevron-menu-group"> + <div className="btn-group-chevron"> + <div className="btn-group"> + + <button className="btn btn-decline" + onClick={this._handleDecline}> + {mozL10n.get("incoming_call_cancel_button")} + </button> + <div className="btn-chevron" onClick={this.toggleDropdownMenu} /> + </div> + + <ul className={dropdownMenuClassesDecline}> + <li className="btn-block" onClick={this._handleDeclineBlock}> + {mozL10n.get("incoming_call_cancel_and_block_button")} + </li> + </ul> + + </div> + </div> + + <div className="fx-embedded-call-button-spacer"></div> + + <AcceptCallButton mode={this._answerModeProps()} /> + + <div className="fx-embedded-call-button-spacer"></div> + + </div> + </div> + ); + /* jshint ignore:end */ + } + }); + + /** + * Incoming call view accept button, renders different primary actions + * (answer with video / with audio only) based on the props received + **/ + var AcceptCallButton = React.createClass({ + + propTypes: { + mode: React.PropTypes.object.isRequired, + }, + + render: function() { + var mode = this.props.mode; + return ( + /* jshint ignore:start */ + <div className="btn-chevron-menu-group"> + <div className="btn-group"> + <button className="btn btn-accept" + onClick={mode.primary.handler} + title={mozL10n.get(mode.primary.tooltip)}> + <span className="fx-embedded-answer-btn-text"> + {mozL10n.get("incoming_call_accept_button")} + </span> + <span className={mode.primary.className}></span> + </button> + <div className={mode.secondary.className} + onClick={mode.secondary.handler} + title={mozL10n.get(mode.secondary.tooltip)}> + </div> + </div> + </div> + /* jshint ignore:end */ + ); + } + }); + + /** + * Something went wrong view. Displayed when there's a big problem. + * + * XXX Based on CallFailedView, but built specially until we flux-ify the + * incoming call views (bug 1088672). + */ + var GenericFailureView = React.createClass({ + mixins: [sharedMixins.AudioMixin], + + propTypes: { + cancelCall: React.PropTypes.func.isRequired + }, + + componentDidMount: function() { + this.play("failure"); + }, + + render: function() { + document.title = mozL10n.get("generic_failure_title"); + + return ( + <div className="call-window"> + <h2>{mozL10n.get("generic_failure_title")}</h2> + + <div className="btn-group call-action-group"> + <button className="btn btn-cancel" + onClick={this.props.cancelCall}> + {mozL10n.get("cancel_button")} + </button> + </div> + </div> + ); + } + }); + + /** + * This view manages the incoming conversation views - from + * call initiation through to the actual conversation and call end. + * + * At the moment, it does more than that, these parts need refactoring out. + */ + var IncomingConversationView = React.createClass({ + mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], + + propTypes: { + client: React.PropTypes.instanceOf(loop.Client).isRequired, + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + conversationAppStore: React.PropTypes.instanceOf( + loop.store.ConversationAppStore).isRequired, + feedbackStore: + React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired + }, + + getInitialState: function() { + return { + callFailed: false, // XXX this should be removed when bug 1047410 lands. + callStatus: "start" + }; + }, + + componentDidMount: function() { + this.props.conversation.on("accept", this.accept, this); + this.props.conversation.on("decline", this.decline, this); + this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); + this.props.conversation.on("call:accepted", this.accepted, this); + this.props.conversation.on("change:publishedStream", this._checkConnected, this); + this.props.conversation.on("change:subscribedStream", this._checkConnected, this); + this.props.conversation.on("session:ended", this.endCall, this); + this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); + this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); + this.props.conversation.on("session:connection-error", this._notifyError, this); + + this.setupIncomingCall(); + }, + + componentDidUnmount: function() { + this.props.conversation.off(null, null, this); + }, + + render: function() { + switch (this.state.callStatus) { + case "start": { + document.title = mozL10n.get("incoming_call_title2"); + + // XXX Don't render anything initially, though this should probably + // be some sort of pending view, whilst we connect the websocket. + return null; + } + case "incoming": { + document.title = mozL10n.get("incoming_call_title2"); + + return ( + <IncomingCallView + model={this.props.conversation} + video={this.props.conversation.hasVideoStream("incoming")} + /> + ); + } + case "connected": { + document.title = this.props.conversation.getCallIdentifier(); + + var callType = this.props.conversation.get("selectedCallType"); + + return ( + <sharedViews.ConversationView + initiate={true} + sdk={this.props.sdk} + model={this.props.conversation} + video={{enabled: callType !== "audio"}} + /> + ); + } + case "end": { + // XXX To be handled with the "failed" view state when bug 1047410 lands + if (this.state.callFailed) { + return <GenericFailureView + cancelCall={this.closeWindow.bind(this)} + />; + } + + document.title = mozL10n.get("conversation_has_ended"); + + this.play("terminated"); + + return ( + <sharedViews.FeedbackView + feedbackStore={this.props.feedbackStore} + onAfterFeedbackReceived={this.closeWindow.bind(this)} + /> + ); + } + case "close": { + this.closeWindow(); + return (<div/>); + } + } + }, + + /** + * Notify the user that the connection was not possible + * @param {{code: number, message: string}} error + */ + _notifyError: function(error) { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + console.error(error); + this.setState({callFailed: true, callStatus: "end"}); + }, + + /** + * Peer hung up. Notifies the user and ends the call. + * + * Event properties: + * - {String} connectionId: OT session id + */ + _onPeerHungup: function() { + this.setState({callFailed: false, callStatus: "end"}); + }, + + /** + * Network disconnected. Notifies the user and ends the call. + */ + _onNetworkDisconnected: function() { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + this.setState({callFailed: true, callStatus: "end"}); + }, + + /** + * Incoming call route. + */ + setupIncomingCall: function() { + navigator.mozLoop.startAlerting(); + + // XXX This is a hack until we rework for the flux model in bug 1088672. + var callData = this.props.conversationAppStore.getStoreState().windowData; + + this.props.conversation.setIncomingSessionData(callData); + this._setupWebSocket(); + }, + + /** + * Starts the actual conversation + */ + accepted: function() { + this.setState({callStatus: "connected"}); + }, + + /** + * Moves the call to the end state + */ + endCall: function() { + navigator.mozLoop.calls.clearCallInProgress( + this.props.conversation.get("windowId")); + this.setState({callStatus: "end"}); + }, + + /** + * Used to set up the web socket connection and navigate to the + * call view if appropriate. + */ + _setupWebSocket: function() { + this._websocket = new loop.CallConnectionWebSocket({ + url: this.props.conversation.get("progressURL"), + websocketToken: this.props.conversation.get("websocketToken"), + callId: this.props.conversation.get("callId"), + }); + this._websocket.promiseConnect().then(function(progressStatus) { + this.setState({ + callStatus: progressStatus === "terminated" ? "close" : "incoming" + }); + }.bind(this), function() { + this._handleSessionError(); + return; + }.bind(this)); + + this._websocket.on("progress", this._handleWebSocketProgress, this); + }, + + /** + * Checks if the streams have been connected, and notifies the + * websocket that the media is now connected. + */ + _checkConnected: function() { + // Check we've had both local and remote streams connected before + // sending the media up message. + if (this.props.conversation.streamsConnected()) { + this._websocket.mediaUp(); + } + }, + + /** + * Used to receive websocket progress and to determine how to handle + * it if appropraite. + * If we add more cases here, then we should refactor this function. + * + * @param {Object} progressData The progress data from the websocket. + * @param {String} previousState The previous state from the websocket. + */ + _handleWebSocketProgress: function(progressData, previousState) { + // We only care about the terminated state at the moment. + if (progressData.state !== "terminated") + return; + + // XXX This would be nicer in the _abortIncomingCall function, but we need to stop + // it here for now due to server-side issues that are being fixed in bug 1088351. + // This is before the abort call to ensure that it happens before the window is + // closed. + navigator.mozLoop.stopAlerting(); + + // If we hit any of the termination reasons, and the user hasn't accepted + // then it seems reasonable to close the window/abort the incoming call. + // + // If the user has accepted the call, and something's happened, display + // the call failed view. + // + // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons + if (previousState === "init" || previousState === "alerting") { + this._abortIncomingCall(); + } else { + this.setState({callFailed: true, callStatus: "end"}); + } + + }, + + /** + * Silently aborts an incoming call - stops the alerting, and + * closes the websocket. + */ + _abortIncomingCall: function() { + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + /** + * Accepts an incoming call. + */ + accept: function() { + navigator.mozLoop.stopAlerting(); + this._websocket.accept(); + this.props.conversation.accepted(); + }, + + /** + * Declines a call and handles closing of the window. + */ + _declineCall: function() { + this._websocket.decline(); + navigator.mozLoop.calls.clearCallInProgress( + this.props.conversation.get("windowId")); + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + /** + * Declines an incoming call. + */ + decline: function() { + navigator.mozLoop.stopAlerting(); + this._declineCall(); + }, + + /** + * Decline and block an incoming call + * @note: + * - loopToken is the callUrl identifier. It gets set in the panel + * after a callUrl is received + */ + declineAndBlock: function() { + navigator.mozLoop.stopAlerting(); + var token = this.props.conversation.get("callToken"); + var callerId = this.props.conversation.get("callerId"); + + // If this is a direct call, we'll need to block the caller directly. + if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) { + navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + // (bug 1103150). + console.log(err.fileName + ":" + err.lineNumber + ": " + err.message); + }); + } else { + this.props.client.deleteCallUrl(token, + this.props.conversation.get("sessionType"), + function(error) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + // (bug 1048909). + console.log(error); + }); + } + + this._declineCall(); + }, + + /** + * Handles a error starting the session + */ + _handleSessionError: function() { + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + console.error("Failed initiating the call session."); + }, + }); + /** * View for pending conversations. Displays a cancel button and appropriate * pending/ringing strings. */ var PendingConversationView = React.createClass({ mixins: [sharedMixins.AudioMixin], propTypes: { @@ -530,13 +1043,16 @@ loop.conversationViews = (function(mozL1 }, }); return { PendingConversationView: PendingConversationView, CallIdentifierView: CallIdentifierView, ConversationDetailView: ConversationDetailView, CallFailedView: CallFailedView, + GenericFailureView: GenericFailureView, + IncomingCallView: IncomingCallView, + IncomingConversationView: IncomingConversationView, OngoingConversationView: OngoingConversationView, OutgoingConversationView: OutgoingConversationView }; })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js +++ b/browser/components/loop/content/js/roomViews.js @@ -290,17 +290,17 @@ loop.roomViews = (function(mozL10n) { "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS }); switch(this.state.roomState) { case ROOM_STATES.FAILED: case ROOM_STATES.FULL: { // Note: While rooms are set to hold a maximum of 2 participants, the // FULL case should never happen on desktop. - return loop.conversation.GenericFailureView({ + return loop.conversationViews.GenericFailureView({ cancelCall: this.closeWindow} ); } case ROOM_STATES.ENDED: { if (this.state.used) return sharedViews.FeedbackView({ feedbackStore: this.props.feedbackStore, onAfterFeedbackReceived: this.closeWindow}
--- a/browser/components/loop/content/js/roomViews.jsx +++ b/browser/components/loop/content/js/roomViews.jsx @@ -290,17 +290,17 @@ loop.roomViews = (function(mozL10n) { "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS }); switch(this.state.roomState) { case ROOM_STATES.FAILED: case ROOM_STATES.FULL: { // Note: While rooms are set to hold a maximum of 2 participants, the // FULL case should never happen on desktop. - return <loop.conversation.GenericFailureView + return <loop.conversationViews.GenericFailureView cancelCall={this.closeWindow} />; } case ROOM_STATES.ENDED: { if (this.state.used) return <sharedViews.FeedbackView feedbackStore={this.props.feedbackStore} onAfterFeedbackReceived={this.closeWindow}
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -2,23 +2,38 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ var expect = chai.expect; describe("loop.conversationViews", function () { "use strict"; var sharedUtils = loop.shared.utils; + var sharedView = loop.shared.views; var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR; var fakeMozLoop, fakeWindow; var CALL_STATES = loop.store.CALL_STATES; + // XXX refactor to Just Work with "sandbox.stubComponent" or else + // just pass in the sandbox and put somewhere generally usable + + function stubComponent(obj, component, mockTagName){ + var reactClass = React.createClass({ + render: function() { + var mockTagName = mockTagName || "div"; + return React.DOM[mockTagName](null, this.props.children); + } + }); + return sandbox.stub(obj, component, reactClass); + } + beforeEach(function() { sandbox = sinon.sandbox.create(); + sandbox.useFakeTimers(); oldTitle = document.title; sandbox.stub(document.mozL10n, "get", function(x) { return x; }); dispatcher = new loop.Dispatcher(); sandbox.stub(dispatcher, "dispatch"); @@ -40,32 +55,45 @@ describe("loop.conversationViews", funct return "audio/ogg"; }, responseType: null, response: new ArrayBuffer(10), onload: null }; fakeMozLoop = navigator.mozLoop = { - getLoopPref: sinon.stub().returns("http://fakeurl"), + // Dummy function, stubbed below. + getLoopPref: function() {}, + calls: { + clearCallInProgress: sinon.stub() + }, composeEmail: sinon.spy(), get appVersionInfo() { return { version: "42", channel: "test", platform: "test" }; }, getAudioBlob: sinon.spy(function(name, callback) { callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"})); }), + startAlerting: sinon.stub(), + stopAlerting: sinon.stub(), userProfile: { email: "bob@invalid.tld" } }; + sinon.stub(fakeMozLoop, "getLoopPref", function(pref) { + if (pref === "fake") { + return"http://fakeurl"; + } + + return false; + }); fakeWindow = { navigator: { mozLoop: fakeMozLoop }, close: sandbox.stub(), }; loop.shared.mixins.setRootObject(fakeWindow); }); @@ -573,9 +601,726 @@ describe("loop.conversationViews", funct loop.conversationViews.PendingConversationView); store.setStoreState({callState: CALL_STATES.TERMINATED}); TestUtils.findRenderedComponentWithType(view, loop.conversationViews.CallFailedView); }); }); + + describe("IncomingConversationView", function() { + var conversationAppStore, conversation, client, icView, oldTitle, + feedbackStore; + + function mountTestComponent() { + return TestUtils.renderIntoDocument( + loop.conversationViews.IncomingConversationView({ + client: client, + conversation: conversation, + sdk: {}, + conversationAppStore: conversationAppStore, + feedbackStore: feedbackStore + })); + } + + beforeEach(function() { + oldTitle = document.title; + client = new loop.Client(); + conversation = new loop.shared.models.ConversationModel({}, { + sdk: {} + }); + conversation.set({windowId: 42}); + var dispatcher = new loop.Dispatcher(); + conversationAppStore = new loop.store.ConversationAppStore({ + dispatcher: dispatcher, + mozLoop: navigator.mozLoop + }); + feedbackStore = new loop.store.FeedbackStore(dispatcher, { + feedbackClient: {} + }); + sandbox.stub(conversation, "setOutgoingSessionData"); + }); + + afterEach(function() { + icView = undefined; + document.title = oldTitle; + }); + + describe("start", function() { + it("should set the title to incoming_call_title2", function() { + conversationAppStore.setStoreState({ + windowData: { + progressURL: "fake", + websocketToken: "fake", + callId: 42 + } + }); + + icView = mountTestComponent(); + + expect(document.title).eql("incoming_call_title2"); + }); + }); + + describe("componentDidMount", function() { + var fakeSessionData, promise, resolveWebSocketConnect; + var rejectWebSocketConnect; + + beforeEach(function() { + fakeSessionData = { + sessionId: "sessionId", + sessionToken: "sessionToken", + apiKey: "apiKey", + callType: "callType", + callId: "Hello", + progressURL: "http://progress.example.com", + websocketToken: "7b" + }; + + conversationAppStore.setStoreState({ + windowData: fakeSessionData + }); + + stubComponent(loop.conversationViews, "IncomingCallView"); + stubComponent(sharedView, "ConversationView"); + }); + + it("should start alerting", function() { + icView = mountTestComponent(); + + sinon.assert.calledOnce(navigator.mozLoop.startAlerting); + }); + + describe("Session Data setup", function() { + beforeEach(function() { + sandbox.stub(loop, "CallConnectionWebSocket").returns({ + promiseConnect: function () { + promise = new Promise(function(resolve, reject) { + resolveWebSocketConnect = resolve; + rejectWebSocketConnect = reject; + }); + return promise; + }, + on: sinon.stub() + }); + }); + + it("should store the session data", function() { + sandbox.stub(conversation, "setIncomingSessionData"); + + icView = mountTestComponent(); + + sinon.assert.calledOnce(conversation.setIncomingSessionData); + sinon.assert.calledWithExactly(conversation.setIncomingSessionData, + fakeSessionData); + }); + + it("should setup the websocket connection", function() { + icView = mountTestComponent(); + + sinon.assert.calledOnce(loop.CallConnectionWebSocket); + sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, { + callId: "Hello", + url: "http://progress.example.com", + websocketToken: "7b" + }); + }); + }); + + describe("WebSocket Handling", function() { + beforeEach(function() { + promise = new Promise(function(resolve, reject) { + resolveWebSocketConnect = resolve; + rejectWebSocketConnect = reject; + }); + + sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); + }); + + it("should set the state to incoming on success", function(done) { + icView = mountTestComponent(); + resolveWebSocketConnect("incoming"); + + promise.then(function () { + expect(icView.state.callStatus).eql("incoming"); + done(); + }); + }); + + it("should set the state to close on success if the progress " + + "state is terminated", function(done) { + icView = mountTestComponent(); + resolveWebSocketConnect("terminated"); + + promise.then(function () { + expect(icView.state.callStatus).eql("close"); + done(); + }); + }); + + // XXX implement me as part of bug 1047410 + // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259 + it.skip("should should switch view state to failed", function(done) { + icView = mountTestComponent(); + rejectWebSocketConnect(); + + promise.then(function() {}, function() { + done(); + }); + }); + }); + + describe("WebSocket Events", function() { + describe("Call cancelled or timed out before acceptance", function() { + beforeEach(function() { + // Mounting the test component automatically calls the required + // setup functions + icView = mountTestComponent(); + promise = new Promise(function(resolve, reject) { + resolve(); + }); + + sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); + sandbox.stub(loop.CallConnectionWebSocket.prototype, "close"); + }); + + describe("progress - terminated (previousState = alerting)", function() { + it("should stop alerting", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "timeout" + }, "alerting"); + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + done(); + }); + }); + + it("should close the websocket", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "closed" + }, "alerting"); + + sinon.assert.calledOnce(icView._websocket.close); + done(); + }); + }); + + it("should close the window", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "answered-elsewhere" + }, "alerting"); + + sandbox.clock.tick(1); + + sinon.assert.calledOnce(fakeWindow.close); + done(); + }); + }); + }); + + + describe("progress - terminated (previousState not init" + + " nor alerting)", + function() { + it("should set the state to end", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "media-fail" + }, "connecting"); + + expect(icView.state.callStatus).eql("end"); + done(); + }); + }); + + it("should stop alerting", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "media-fail" + }, "connecting"); + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + done(); + }); + }); + }); + }); + }); + + describe("#accept", function() { + beforeEach(function() { + icView = mountTestComponent(); + conversation.setIncomingSessionData({ + sessionId: "sessionId", + sessionToken: "sessionToken", + apiKey: "apiKey", + callType: "callType", + callId: "Hello", + progressURL: "http://progress.example.com", + websocketToken: 123 + }); + + sandbox.stub(icView._websocket, "accept"); + sandbox.stub(icView.props.conversation, "accepted"); + }); + + it("should initiate the conversation", function() { + icView.accept(); + + sinon.assert.calledOnce(icView.props.conversation.accepted); + }); + + it("should notify the websocket of the user acceptance", function() { + icView.accept(); + + sinon.assert.calledOnce(icView._websocket.accept); + }); + + it("should stop alerting", function() { + icView.accept(); + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + }); + }); + + describe("#decline", function() { + beforeEach(function() { + icView = mountTestComponent(); + + icView._websocket = { + decline: sinon.stub(), + close: sinon.stub() + }; + conversation.set({ + windowId: "8699" + }); + conversation.setIncomingSessionData({ + websocketToken: 123 + }); + }); + + it("should close the window", function() { + icView.decline(); + + sandbox.clock.tick(1); + + sinon.assert.calledOnce(fakeWindow.close); + }); + + it("should stop alerting", function() { + icView.decline(); + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + }); + + it("should release callData", function() { + icView.decline(); + + sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress); + sinon.assert.calledWithExactly( + navigator.mozLoop.calls.clearCallInProgress, "8699"); + }); + }); + + describe("#blocked", function() { + var mozLoop, deleteCallUrlStub; + + beforeEach(function() { + icView = mountTestComponent(); + + icView._websocket = { + decline: sinon.spy(), + close: sinon.stub() + }; + + mozLoop = { + LOOP_SESSION_TYPE: { + GUEST: 1, + FXA: 2 + } + }; + + deleteCallUrlStub = sandbox.stub(loop.Client.prototype, + "deleteCallUrl"); + }); + + it("should call mozLoop.stopAlerting", function() { + icView.declineAndBlock(); + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + }); + + it("should call delete call", function() { + sandbox.stub(conversation, "get").withArgs("callToken") + .returns("fakeToken") + .withArgs("sessionType") + .returns(mozLoop.LOOP_SESSION_TYPE.FXA); + + icView.declineAndBlock(); + + sinon.assert.calledOnce(deleteCallUrlStub); + sinon.assert.calledWithExactly(deleteCallUrlStub, + "fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func); + }); + + it("should get callToken from conversation model", function() { + sandbox.stub(conversation, "get"); + icView.declineAndBlock(); + + sinon.assert.called(conversation.get); + sinon.assert.calledWithExactly(conversation.get, "callToken"); + sinon.assert.calledWithExactly(conversation.get, "windowId"); + }); + + it("should trigger error handling in case of error", function() { + // XXX just logging to console for now + var log = sandbox.stub(console, "log"); + var fakeError = { + error: true + }; + deleteCallUrlStub.callsArgWith(2, fakeError); + icView.declineAndBlock(); + + sinon.assert.calledOnce(log); + sinon.assert.calledWithExactly(log, fakeError); + }); + + it("should close the window", function() { + icView.declineAndBlock(); + + sandbox.clock.tick(1); + + sinon.assert.calledOnce(fakeWindow.close); + }); + }); + }); + + describe("Events", function() { + var fakeSessionData; + + beforeEach(function() { + + fakeSessionData = { + sessionId: "sessionId", + sessionToken: "sessionToken", + apiKey: "apiKey" + }; + + conversationAppStore.setStoreState({ + windowData: fakeSessionData + }); + + sandbox.stub(conversation, "setIncomingSessionData"); + sandbox.stub(loop, "CallConnectionWebSocket").returns({ + promiseConnect: function() { + return new Promise(function() {}); + }, + on: sandbox.spy() + }); + + icView = mountTestComponent(); + + conversation.set("loopToken", "fakeToken"); + stubComponent(sharedView, "ConversationView"); + }); + + describe("call:accepted", function() { + it("should display the ConversationView", + function() { + conversation.accepted(); + + TestUtils.findRenderedComponentWithType(icView, + sharedView.ConversationView); + }); + + it("should set the title to the call identifier", function() { + sandbox.stub(conversation, "getCallIdentifier").returns("fakeId"); + + conversation.accepted(); + + expect(document.title).eql("fakeId"); + }); + }); + + describe("session:ended", function() { + it("should display the feedback view when the call session ends", + function() { + conversation.trigger("session:ended"); + + TestUtils.findRenderedComponentWithType(icView, + sharedView.FeedbackView); + }); + }); + + describe("session:peer-hungup", function() { + it("should display the feedback view when the peer hangs up", + function() { + conversation.trigger("session:peer-hungup"); + + TestUtils.findRenderedComponentWithType(icView, + sharedView.FeedbackView); + }); + }); + + describe("session:network-disconnected", function() { + it("should navigate to call failed when network disconnects", + function() { + conversation.trigger("session:network-disconnected"); + + TestUtils.findRenderedComponentWithType(icView, + loop.conversationViews.GenericFailureView); + }); + + it("should update the conversation window toolbar title", + function() { + conversation.trigger("session:network-disconnected"); + + expect(document.title).eql("generic_failure_title"); + }); + }); + + describe("Published and Subscribed Streams", function() { + beforeEach(function() { + icView._websocket = { + mediaUp: sinon.spy() + }; + }); + + describe("publishStream", function() { + it("should not notify the websocket if only one stream is up", + function() { + conversation.set("publishedStream", true); + + sinon.assert.notCalled(icView._websocket.mediaUp); + }); + + it("should notify the websocket that media is up if both streams" + + "are connected", function() { + conversation.set("subscribedStream", true); + conversation.set("publishedStream", true); + + sinon.assert.calledOnce(icView._websocket.mediaUp); + }); + }); + + describe("subscribedStream", function() { + it("should not notify the websocket if only one stream is up", + function() { + conversation.set("subscribedStream", true); + + sinon.assert.notCalled(icView._websocket.mediaUp); + }); + + it("should notify the websocket that media is up if both streams" + + "are connected", function() { + conversation.set("publishedStream", true); + conversation.set("subscribedStream", true); + + sinon.assert.calledOnce(icView._websocket.mediaUp); + }); + }); + }); + }); + }); + + describe("IncomingCallView", function() { + var view, model, fakeAudio; + + beforeEach(function() { + var Model = Backbone.Model.extend({ + getCallIdentifier: function() {return "fakeId";} + }); + model = new Model(); + sandbox.spy(model, "trigger"); + sandbox.stub(model, "set"); + + fakeAudio = { + play: sinon.spy(), + pause: sinon.spy(), + removeAttribute: sinon.spy() + }; + sandbox.stub(window, "Audio").returns(fakeAudio); + + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: true + })); + }); + + describe("default answer mode", function() { + it("should display video as primary answer mode", function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: true + })); + var primaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-icon-video'); + + expect(primaryBtn).not.to.eql(null); + }); + + it("should display audio as primary answer mode", function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: false + })); + var primaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-icon-audio'); + + expect(primaryBtn).not.to.eql(null); + }); + + it("should accept call with video", function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: true + })); + var primaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-icon-video'); + + React.addons.TestUtils.Simulate.click(primaryBtn); + + sinon.assert.calledOnce(model.set); + sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video"); + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWithExactly(model.trigger, "accept"); + }); + + it("should accept call with audio", function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: false + })); + var primaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-icon-audio'); + + React.addons.TestUtils.Simulate.click(primaryBtn); + + sinon.assert.calledOnce(model.set); + sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio"); + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWithExactly(model.trigger, "accept"); + }); + + it("should accept call with video when clicking on secondary btn", + function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: false + })); + var secondaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-video-small'); + + React.addons.TestUtils.Simulate.click(secondaryBtn); + + sinon.assert.calledOnce(model.set); + sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video"); + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWithExactly(model.trigger, "accept"); + }); + + it("should accept call with audio when clicking on secondary btn", + function() { + view = TestUtils.renderIntoDocument( + loop.conversationViews.IncomingCallView({ + model: model, + video: true + })); + var secondaryBtn = view.getDOMNode() + .querySelector('.fx-embedded-btn-audio-small'); + + React.addons.TestUtils.Simulate.click(secondaryBtn); + + sinon.assert.calledOnce(model.set); + sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio"); + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWithExactly(model.trigger, "accept"); + }); + }); + + describe("click event on .btn-accept", function() { + it("should trigger an 'accept' conversation model event", function () { + var buttonAccept = view.getDOMNode().querySelector(".btn-accept"); + model.trigger.withArgs("accept"); + TestUtils.Simulate.click(buttonAccept); + + /* Setting a model property triggers 2 events */ + sinon.assert.calledOnce(model.trigger.withArgs("accept")); + }); + + it("should set selectedCallType to audio-video", function () { + var buttonAccept = view.getDOMNode().querySelector(".btn-accept"); + + TestUtils.Simulate.click(buttonAccept); + + sinon.assert.calledOnce(model.set); + sinon.assert.calledWithExactly(model.set, "selectedCallType", + "audio-video"); + }); + }); + + describe("click event on .btn-decline", function() { + it("should trigger an 'decline' conversation model event", function() { + var buttonDecline = view.getDOMNode().querySelector(".btn-decline"); + + TestUtils.Simulate.click(buttonDecline); + + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWith(model.trigger, "decline"); + }); + }); + + describe("click event on .btn-block", function() { + it("should trigger a 'block' conversation model event", function() { + var buttonBlock = view.getDOMNode().querySelector(".btn-block"); + + TestUtils.Simulate.click(buttonBlock); + + sinon.assert.calledOnce(model.trigger); + sinon.assert.calledWith(model.trigger, "declineAndBlock"); + }); + }); + }); + + describe("GenericFailureView", function() { + var view, fakeAudio; + + beforeEach(function() { + fakeAudio = { + play: sinon.spy(), + pause: sinon.spy(), + removeAttribute: sinon.spy() + }; + navigator.mozLoop.doNotDisturb = false; + sandbox.stub(window, "Audio").returns(fakeAudio); + + view = TestUtils.renderIntoDocument( + loop.conversationViews.GenericFailureView({ + cancelCall: function() {} + }) + ); + }); + + it("should play a failure sound, once", function() { + sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob); + sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob, + "failure", sinon.match.func); + sinon.assert.calledOnce(fakeAudio.play); + expect(fakeAudio.loop).to.equal(false); + }); + + }); });
--- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -5,36 +5,21 @@ /* global loop, sinon, React, TestUtils */ var expect = chai.expect; describe("loop.conversation", function() { "use strict"; var sharedModels = loop.shared.models, - sharedView = loop.shared.views, fakeWindow, sandbox; - // XXX refactor to Just Work with "sandbox.stubComponent" or else - // just pass in the sandbox and put somewhere generally usable - - function stubComponent(obj, component, mockTagName){ - var reactClass = React.createClass({ - render: function() { - var mockTagName = mockTagName || "div"; - return React.DOM[mockTagName](null, this.props.children); - } - }); - return sandbox.stub(obj, component, reactClass); - } - beforeEach(function() { sandbox = sinon.sandbox.create(); - sandbox.useFakeTimers(); navigator.mozLoop = { doNotDisturb: true, getStrings: function() { return JSON.stringify({textContent: "fakeText"}); }, get locale() { return "en-US"; @@ -42,19 +27,16 @@ describe("loop.conversation", function() setLoopPref: sinon.stub(), getLoopPref: function(prefName) { if (prefName == "debug.sdk") { return false; } return "http://fake"; }, - calls: { - clearCallInProgress: sinon.stub() - }, LOOP_SESSION_TYPE: { GUEST: 1, FXA: 2 }, startAlerting: sinon.stub(), stopAlerting: sinon.stub(), ensureRegistered: sinon.stub(), get appVersionInfo() { @@ -215,17 +197,17 @@ describe("loop.conversation", function() }, on: sandbox.spy() }); conversationAppStore.setStoreState({windowType: "incoming"}); ccView = mountTestComponent(); TestUtils.findRenderedComponentWithType(ccView, - loop.conversation.IncomingConversationView); + loop.conversationViews.IncomingConversationView); }); it("should display the RoomView for rooms", function() { conversationAppStore.setStoreState({windowType: "room"}); ccView = mountTestComponent(); TestUtils.findRenderedComponentWithType(ccView, @@ -233,722 +215,12 @@ describe("loop.conversation", function() }); it("should display the GenericFailureView for failures", function() { conversationAppStore.setStoreState({windowType: "failed"}); ccView = mountTestComponent(); TestUtils.findRenderedComponentWithType(ccView, - loop.conversation.GenericFailureView); + loop.conversationViews.GenericFailureView); }); }); - - describe("IncomingConversationView", function() { - var conversationAppStore, conversation, client, icView, oldTitle, - feedbackStore; - - function mountTestComponent() { - return TestUtils.renderIntoDocument( - loop.conversation.IncomingConversationView({ - client: client, - conversation: conversation, - sdk: {}, - conversationAppStore: conversationAppStore, - feedbackStore: feedbackStore - })); - } - - beforeEach(function() { - oldTitle = document.title; - client = new loop.Client(); - conversation = new loop.shared.models.ConversationModel({}, { - sdk: {} - }); - conversation.set({windowId: 42}); - var dispatcher = new loop.Dispatcher(); - conversationAppStore = new loop.store.ConversationAppStore({ - dispatcher: dispatcher, - mozLoop: navigator.mozLoop - }); - feedbackStore = new loop.store.FeedbackStore(dispatcher, { - feedbackClient: {} - }); - sandbox.stub(conversation, "setOutgoingSessionData"); - }); - - afterEach(function() { - icView = undefined; - document.title = oldTitle; - }); - - describe("start", function() { - it("should set the title to incoming_call_title2", function() { - conversationAppStore.setStoreState({ - windowData: { - progressURL: "fake", - websocketToken: "fake", - callId: 42 - } - }); - - icView = mountTestComponent(); - - expect(document.title).eql("incoming_call_title2"); - }); - }); - - describe("componentDidMount", function() { - var fakeSessionData, promise, resolveWebSocketConnect; - var rejectWebSocketConnect; - - beforeEach(function() { - fakeSessionData = { - sessionId: "sessionId", - sessionToken: "sessionToken", - apiKey: "apiKey", - callType: "callType", - callId: "Hello", - progressURL: "http://progress.example.com", - websocketToken: "7b" - }; - - conversationAppStore.setStoreState({ - windowData: fakeSessionData - }); - - stubComponent(loop.conversation, "IncomingCallView"); - stubComponent(sharedView, "ConversationView"); - }); - - it("should start alerting", function() { - icView = mountTestComponent(); - - sinon.assert.calledOnce(navigator.mozLoop.startAlerting); - }); - - describe("Session Data setup", function() { - beforeEach(function() { - sandbox.stub(loop, "CallConnectionWebSocket").returns({ - promiseConnect: function () { - promise = new Promise(function(resolve, reject) { - resolveWebSocketConnect = resolve; - rejectWebSocketConnect = reject; - }); - return promise; - }, - on: sinon.stub() - }); - }); - - it("should store the session data", function() { - sandbox.stub(conversation, "setIncomingSessionData"); - - icView = mountTestComponent(); - - sinon.assert.calledOnce(conversation.setIncomingSessionData); - sinon.assert.calledWithExactly(conversation.setIncomingSessionData, - fakeSessionData); - }); - - it("should setup the websocket connection", function() { - icView = mountTestComponent(); - - sinon.assert.calledOnce(loop.CallConnectionWebSocket); - sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, { - callId: "Hello", - url: "http://progress.example.com", - websocketToken: "7b" - }); - }); - }); - - describe("WebSocket Handling", function() { - beforeEach(function() { - promise = new Promise(function(resolve, reject) { - resolveWebSocketConnect = resolve; - rejectWebSocketConnect = reject; - }); - - sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); - }); - - it("should set the state to incoming on success", function(done) { - icView = mountTestComponent(); - resolveWebSocketConnect("incoming"); - - promise.then(function () { - expect(icView.state.callStatus).eql("incoming"); - done(); - }); - }); - - it("should set the state to close on success if the progress " + - "state is terminated", function(done) { - icView = mountTestComponent(); - resolveWebSocketConnect("terminated"); - - promise.then(function () { - expect(icView.state.callStatus).eql("close"); - done(); - }); - }); - - // XXX implement me as part of bug 1047410 - // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259 - it.skip("should should switch view state to failed", function(done) { - icView = mountTestComponent(); - rejectWebSocketConnect(); - - promise.then(function() {}, function() { - done(); - }); - }); - }); - - describe("WebSocket Events", function() { - describe("Call cancelled or timed out before acceptance", function() { - beforeEach(function() { - // Mounting the test component automatically calls the required - // setup functions - icView = mountTestComponent(); - promise = new Promise(function(resolve, reject) { - resolve(); - }); - - sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); - sandbox.stub(loop.CallConnectionWebSocket.prototype, "close"); - }); - - describe("progress - terminated (previousState = alerting)", function() { - it("should stop alerting", function(done) { - promise.then(function() { - icView._websocket.trigger("progress", { - state: "terminated", - reason: "timeout" - }, "alerting"); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - done(); - }); - }); - - it("should close the websocket", function(done) { - promise.then(function() { - icView._websocket.trigger("progress", { - state: "terminated", - reason: "closed" - }, "alerting"); - - sinon.assert.calledOnce(icView._websocket.close); - done(); - }); - }); - - it("should close the window", function(done) { - promise.then(function() { - icView._websocket.trigger("progress", { - state: "terminated", - reason: "answered-elsewhere" - }, "alerting"); - - sandbox.clock.tick(1); - - sinon.assert.calledOnce(fakeWindow.close); - done(); - }); - }); - }); - - - describe("progress - terminated (previousState not init" + - " nor alerting)", - function() { - it("should set the state to end", function(done) { - promise.then(function() { - icView._websocket.trigger("progress", { - state: "terminated", - reason: "media-fail" - }, "connecting"); - - expect(icView.state.callStatus).eql("end"); - done(); - }); - }); - - it("should stop alerting", function(done) { - promise.then(function() { - icView._websocket.trigger("progress", { - state: "terminated", - reason: "media-fail" - }, "connecting"); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - done(); - }); - }); - }); - }); - }); - - describe("#accept", function() { - beforeEach(function() { - icView = mountTestComponent(); - conversation.setIncomingSessionData({ - sessionId: "sessionId", - sessionToken: "sessionToken", - apiKey: "apiKey", - callType: "callType", - callId: "Hello", - progressURL: "http://progress.example.com", - websocketToken: 123 - }); - - sandbox.stub(icView._websocket, "accept"); - sandbox.stub(icView.props.conversation, "accepted"); - }); - - it("should initiate the conversation", function() { - icView.accept(); - - sinon.assert.calledOnce(icView.props.conversation.accepted); - }); - - it("should notify the websocket of the user acceptance", function() { - icView.accept(); - - sinon.assert.calledOnce(icView._websocket.accept); - }); - - it("should stop alerting", function() { - icView.accept(); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - }); - }); - - describe("#decline", function() { - beforeEach(function() { - icView = mountTestComponent(); - - icView._websocket = { - decline: sinon.stub(), - close: sinon.stub() - }; - conversation.set({ - windowId: "8699" - }); - conversation.setIncomingSessionData({ - websocketToken: 123 - }); - }); - - it("should close the window", function() { - icView.decline(); - - sandbox.clock.tick(1); - - sinon.assert.calledOnce(fakeWindow.close); - }); - - it("should stop alerting", function() { - icView.decline(); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - }); - - it("should release callData", function() { - icView.decline(); - - sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress); - sinon.assert.calledWithExactly( - navigator.mozLoop.calls.clearCallInProgress, "8699"); - }); - }); - - describe("#blocked", function() { - var mozLoop, deleteCallUrlStub; - - beforeEach(function() { - icView = mountTestComponent(); - - icView._websocket = { - decline: sinon.spy(), - close: sinon.stub() - }; - - mozLoop = { - LOOP_SESSION_TYPE: { - GUEST: 1, - FXA: 2 - } - }; - - deleteCallUrlStub = sandbox.stub(loop.Client.prototype, - "deleteCallUrl"); - }); - - it("should call mozLoop.stopAlerting", function() { - icView.declineAndBlock(); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - }); - - it("should call delete call", function() { - sandbox.stub(conversation, "get").withArgs("callToken") - .returns("fakeToken") - .withArgs("sessionType") - .returns(mozLoop.LOOP_SESSION_TYPE.FXA); - - icView.declineAndBlock(); - - sinon.assert.calledOnce(deleteCallUrlStub); - sinon.assert.calledWithExactly(deleteCallUrlStub, - "fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func); - }); - - it("should get callToken from conversation model", function() { - sandbox.stub(conversation, "get"); - icView.declineAndBlock(); - - sinon.assert.called(conversation.get); - sinon.assert.calledWithExactly(conversation.get, "callToken"); - sinon.assert.calledWithExactly(conversation.get, "windowId"); - }); - - it("should trigger error handling in case of error", function() { - // XXX just logging to console for now - var log = sandbox.stub(console, "log"); - var fakeError = { - error: true - }; - deleteCallUrlStub.callsArgWith(2, fakeError); - icView.declineAndBlock(); - - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, fakeError); - }); - - it("should close the window", function() { - icView.declineAndBlock(); - - sandbox.clock.tick(1); - - sinon.assert.calledOnce(fakeWindow.close); - }); - }); - }); - - describe("Events", function() { - var fakeSessionData; - - beforeEach(function() { - - fakeSessionData = { - sessionId: "sessionId", - sessionToken: "sessionToken", - apiKey: "apiKey" - }; - - conversationAppStore.setStoreState({ - windowData: fakeSessionData - }); - - sandbox.stub(conversation, "setIncomingSessionData"); - sandbox.stub(loop, "CallConnectionWebSocket").returns({ - promiseConnect: function() { - return new Promise(function() {}); - }, - on: sandbox.spy() - }); - - icView = mountTestComponent(); - - conversation.set("loopToken", "fakeToken"); - stubComponent(sharedView, "ConversationView"); - }); - - describe("call:accepted", function() { - it("should display the ConversationView", - function() { - conversation.accepted(); - - TestUtils.findRenderedComponentWithType(icView, - sharedView.ConversationView); - }); - - it("should set the title to the call identifier", function() { - sandbox.stub(conversation, "getCallIdentifier").returns("fakeId"); - - conversation.accepted(); - - expect(document.title).eql("fakeId"); - }); - }); - - describe("session:ended", function() { - it("should display the feedback view when the call session ends", - function() { - conversation.trigger("session:ended"); - - TestUtils.findRenderedComponentWithType(icView, - sharedView.FeedbackView); - }); - }); - - describe("session:peer-hungup", function() { - it("should display the feedback view when the peer hangs up", - function() { - conversation.trigger("session:peer-hungup"); - - TestUtils.findRenderedComponentWithType(icView, - sharedView.FeedbackView); - }); - }); - - describe("session:network-disconnected", function() { - it("should navigate to call failed when network disconnects", - function() { - conversation.trigger("session:network-disconnected"); - - TestUtils.findRenderedComponentWithType(icView, - loop.conversation.GenericFailureView); - }); - - it("should update the conversation window toolbar title", - function() { - conversation.trigger("session:network-disconnected"); - - expect(document.title).eql("generic_failure_title"); - }); - }); - - describe("Published and Subscribed Streams", function() { - beforeEach(function() { - icView._websocket = { - mediaUp: sinon.spy() - }; - }); - - describe("publishStream", function() { - it("should not notify the websocket if only one stream is up", - function() { - conversation.set("publishedStream", true); - - sinon.assert.notCalled(icView._websocket.mediaUp); - }); - - it("should notify the websocket that media is up if both streams" + - "are connected", function() { - conversation.set("subscribedStream", true); - conversation.set("publishedStream", true); - - sinon.assert.calledOnce(icView._websocket.mediaUp); - }); - }); - - describe("subscribedStream", function() { - it("should not notify the websocket if only one stream is up", - function() { - conversation.set("subscribedStream", true); - - sinon.assert.notCalled(icView._websocket.mediaUp); - }); - - it("should notify the websocket that media is up if both streams" + - "are connected", function() { - conversation.set("publishedStream", true); - conversation.set("subscribedStream", true); - - sinon.assert.calledOnce(icView._websocket.mediaUp); - }); - }); - }); - }); - }); - - describe("IncomingCallView", function() { - var view, model, fakeAudio; - - beforeEach(function() { - var Model = Backbone.Model.extend({ - getCallIdentifier: function() {return "fakeId";} - }); - model = new Model(); - sandbox.spy(model, "trigger"); - sandbox.stub(model, "set"); - - fakeAudio = { - play: sinon.spy(), - pause: sinon.spy(), - removeAttribute: sinon.spy() - }; - sandbox.stub(window, "Audio").returns(fakeAudio); - - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: true - })); - }); - - describe("default answer mode", function() { - it("should display video as primary answer mode", function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: true - })); - var primaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-icon-video'); - - expect(primaryBtn).not.to.eql(null); - }); - - it("should display audio as primary answer mode", function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: false - })); - var primaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-icon-audio'); - - expect(primaryBtn).not.to.eql(null); - }); - - it("should accept call with video", function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: true - })); - var primaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-icon-video'); - - React.addons.TestUtils.Simulate.click(primaryBtn); - - sinon.assert.calledOnce(model.set); - sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video"); - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWithExactly(model.trigger, "accept"); - }); - - it("should accept call with audio", function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: false - })); - var primaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-icon-audio'); - - React.addons.TestUtils.Simulate.click(primaryBtn); - - sinon.assert.calledOnce(model.set); - sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio"); - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWithExactly(model.trigger, "accept"); - }); - - it("should accept call with video when clicking on secondary btn", - function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: false - })); - var secondaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-video-small'); - - React.addons.TestUtils.Simulate.click(secondaryBtn); - - sinon.assert.calledOnce(model.set); - sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video"); - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWithExactly(model.trigger, "accept"); - }); - - it("should accept call with audio when clicking on secondary btn", - function() { - view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ - model: model, - video: true - })); - var secondaryBtn = view.getDOMNode() - .querySelector('.fx-embedded-btn-audio-small'); - - React.addons.TestUtils.Simulate.click(secondaryBtn); - - sinon.assert.calledOnce(model.set); - sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio"); - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWithExactly(model.trigger, "accept"); - }); - }); - - describe("click event on .btn-accept", function() { - it("should trigger an 'accept' conversation model event", function () { - var buttonAccept = view.getDOMNode().querySelector(".btn-accept"); - model.trigger.withArgs("accept"); - TestUtils.Simulate.click(buttonAccept); - - /* Setting a model property triggers 2 events */ - sinon.assert.calledOnce(model.trigger.withArgs("accept")); - }); - - it("should set selectedCallType to audio-video", function () { - var buttonAccept = view.getDOMNode().querySelector(".btn-accept"); - - TestUtils.Simulate.click(buttonAccept); - - sinon.assert.calledOnce(model.set); - sinon.assert.calledWithExactly(model.set, "selectedCallType", - "audio-video"); - }); - }); - - describe("click event on .btn-decline", function() { - it("should trigger an 'decline' conversation model event", function() { - var buttonDecline = view.getDOMNode().querySelector(".btn-decline"); - - TestUtils.Simulate.click(buttonDecline); - - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWith(model.trigger, "decline"); - }); - }); - - describe("click event on .btn-block", function() { - it("should trigger a 'block' conversation model event", function() { - var buttonBlock = view.getDOMNode().querySelector(".btn-block"); - - TestUtils.Simulate.click(buttonBlock); - - sinon.assert.calledOnce(model.trigger); - sinon.assert.calledWith(model.trigger, "declineAndBlock"); - }); - }); - }); - - describe("GenericFailureView", function() { - var view, fakeAudio; - - beforeEach(function() { - fakeAudio = { - play: sinon.spy(), - pause: sinon.spy(), - removeAttribute: sinon.spy() - }; - navigator.mozLoop.doNotDisturb = false; - sandbox.stub(window, "Audio").returns(fakeAudio); - - view = TestUtils.renderIntoDocument( - loop.conversation.GenericFailureView({ - cancelCall: function() {} - }) - ); - }); - - it("should play a failure sound, once", function() { - sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob); - sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob, - "failure", sinon.match.func); - sinon.assert.calledOnce(fakeAudio.play); - expect(fakeAudio.loop).to.equal(false); - }); - - }); });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js +++ b/browser/components/loop/test/desktop-local/roomViews_test.js @@ -316,27 +316,27 @@ describe("loop.roomViews", function () { it("should render the GenericFailureView if the roomState is `FAILED`", function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED}); view = mountTestComponent(); TestUtils.findRenderedComponentWithType(view, - loop.conversation.GenericFailureView); + loop.conversationViews.GenericFailureView); }); it("should render the GenericFailureView if the roomState is `FULL`", function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL}); view = mountTestComponent(); TestUtils.findRenderedComponentWithType(view, - loop.conversation.GenericFailureView); + loop.conversationViews.GenericFailureView); }); it("should render the DesktopRoomInvitationView if roomState is `JOINED`", function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED}); view = mountTestComponent();
--- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -13,17 +13,17 @@ // Stop the default init functions running to avoid conflicts. document.removeEventListener('DOMContentLoaded', loop.panel.init); document.removeEventListener('DOMContentLoaded', loop.conversation.init); // 1. Desktop components // 1.1 Panel var PanelView = loop.panel.PanelView; // 1.2. Conversation Window - var IncomingCallView = loop.conversation.IncomingCallView; + var IncomingCallView = loop.conversationViews.IncomingCallView; var DesktopPendingConversationView = loop.conversationViews.PendingConversationView; var CallFailedView = loop.conversationViews.CallFailedView; var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView; // 2. Standalone webapp var HomeView = loop.webapp.HomeView; var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
--- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -13,17 +13,17 @@ // Stop the default init functions running to avoid conflicts. document.removeEventListener('DOMContentLoaded', loop.panel.init); document.removeEventListener('DOMContentLoaded', loop.conversation.init); // 1. Desktop components // 1.1 Panel var PanelView = loop.panel.PanelView; // 1.2. Conversation Window - var IncomingCallView = loop.conversation.IncomingCallView; + var IncomingCallView = loop.conversationViews.IncomingCallView; var DesktopPendingConversationView = loop.conversationViews.PendingConversationView; var CallFailedView = loop.conversationViews.CallFailedView; var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView; // 2. Standalone webapp var HomeView = loop.webapp.HomeView; var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
--- a/browser/devtools/fontinspector/font-inspector.js +++ b/browser/devtools/fontinspector/font-inspector.js @@ -80,17 +80,17 @@ FontInspector.prototype = { */ undim: function FI_undim() { this.chromeDoc.body.classList.remove("dim"); }, /** * Retrieve all the font info for the selected node and display it. */ - update: Task.async(function*() { + update: Task.async(function*(showAllFonts) { let node = this.inspector.selection.nodeFront; if (!node || !this.isActive() || !this.inspector.selection.isConnected() || !this.inspector.selection.isElementNode() || this.chromeDoc.body.classList.contains("dim")) { return; @@ -99,20 +99,26 @@ FontInspector.prototype = { this.chromeDoc.querySelector("#all-fonts").innerHTML = ""; let fillStyle = (Services.prefs.getCharPref("devtools.theme") == "light") ? "black" : "white"; let options = { includePreviews: true, previewFillStyle: fillStyle } - - let fonts = yield this.pageStyle.getUsedFontFaces(node, options) + let fonts = []; + if (showAllFonts){ + fonts = yield this.pageStyle.getAllUsedFontFaces(options) .then(null, console.error); - if (!fonts) { + } + else{ + fonts = yield this.pageStyle.getUsedFontFaces(node, options) + .then(null, console.error); + } + if (!fonts || !fonts.length) { return; } for (let font of fonts) { font.previewUrl = yield font.preview.data.string(); } // in case we've been destroyed in the meantime @@ -164,31 +170,20 @@ FontInspector.prototype = { } let preview = s.querySelector(".font-preview"); preview.src = font.previewUrl; this.chromeDoc.querySelector("#all-fonts").appendChild(s); }, /** - * Select the <body> to show all the fonts included in the document. + * Show all fonts for the document (including iframes) */ showAll: function FI_showAll() { - if (!this.isActive() || - !this.inspector.selection.isConnected() || - !this.inspector.selection.isElementNode()) { - return; - } - - // Select the body node to show all fonts - let walker = this.inspector.walker; - - walker.getRootNode().then(root => walker.querySelector(root, "body")).then(body => { - this.inspector.selection.setNodeFront(body, "fontinspector"); - }); + this.update(true); }, } window.setPanel = function(panel) { window.fontInspector = new FontInspector(panel, window); } window.onunload = function() {
--- a/browser/devtools/fontinspector/test/browser.ini +++ b/browser/devtools/fontinspector/test/browser.ini @@ -1,9 +1,10 @@ [DEFAULT] subsuite = devtools support-files = browser_fontinspector.html + test_iframe.html ostrich-black.ttf ostrich-regular.ttf head.js [browser_fontinspector.js]
--- a/browser/devtools/fontinspector/test/browser_fontinspector.html +++ b/browser/devtools/fontinspector/test/browser_fontinspector.html @@ -40,12 +40,13 @@ font-family: bar; font-weight: 800; } </style> <body> BODY <div>DIV</div> + <iframe src="test_iframe.html"></iframe> <div class="normal-text">NORMAL DIV</div> <div class="bold-text">BOLD DIV</div> <div class="black-text">800 DIV</div> </body>
--- a/browser/devtools/fontinspector/test/browser_fontinspector.js +++ b/browser/devtools/fontinspector/test/browser_fontinspector.js @@ -102,12 +102,13 @@ function* testDivFonts(inspector) { function* testShowAllFonts(inspector) { info("testing showing all fonts"); let updated = inspector.once("fontinspector-updated"); viewDoc.querySelector("#showall").click(); yield updated; - is(inspector.selection.nodeFront.nodeName, "BODY", "Show all fonts selected the body node"); + // shouldn't change the node selection + is(inspector.selection.nodeFront.nodeName, "DIV", "Show all fonts selected"); let sections = viewDoc.querySelectorAll("#all-fonts > section"); - is(sections.length, 5, "And font-inspector still shows 5 fonts for body"); + is(sections.length, 6, "Font inspector shows 6 fonts (1 from iframe)"); }
new file mode 100644 --- /dev/null +++ b/browser/devtools/fontinspector/test/test_iframe.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<style> + div{ + font-family: "Times New Roman"; + } +</style> + +<body> + <div>Hello world</div> +</body>
--- a/browser/extensions/pdfjs/README.mozilla +++ b/browser/extensions/pdfjs/README.mozilla @@ -1,4 +1,4 @@ This is the pdf.js project output, https://github.com/mozilla/pdf.js -Current extension version is: 1.0.978 +Current extension version is: 1.0.1040
--- a/browser/extensions/pdfjs/content/PdfJs.jsm +++ b/browser/extensions/pdfjs/content/PdfJs.jsm @@ -1,56 +1,64 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ +/* jshint esnext:true */ +/* globals Components, Services, XPCOMUtils, PdfjsChromeUtils, PdfRedirector, + PdfjsContentUtils, DEFAULT_PREFERENCES, PdfStreamConverter */ -var EXPORTED_SYMBOLS = ["PdfJs"]; +'use strict'; + +var EXPORTED_SYMBOLS = ['PdfJs']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cm = Components.manager; const Cu = Components.utils; const PREF_PREFIX = 'pdfjs'; const PREF_DISABLED = PREF_PREFIX + '.disabled'; const PREF_MIGRATION_VERSION = PREF_PREFIX + '.migrationVersion'; const PREF_PREVIOUS_ACTION = PREF_PREFIX + '.previousHandler.preferredAction'; -const PREF_PREVIOUS_ASK = PREF_PREFIX + '.previousHandler.alwaysAskBeforeHandling'; +const PREF_PREVIOUS_ASK = PREF_PREFIX + + '.previousHandler.alwaysAskBeforeHandling'; const PREF_DISABLED_PLUGIN_TYPES = 'plugin.disable_full_page_plugin_for_types'; const TOPIC_PDFJS_HANDLER_CHANGED = 'pdfjs:handlerChanged'; -const TOPIC_PLUGINS_LIST_UPDATED = "plugins-list-updated"; -const TOPIC_PLUGIN_INFO_UPDATED = "plugin-info-updated"; +const TOPIC_PLUGINS_LIST_UPDATED = 'plugins-list-updated'; +const TOPIC_PLUGIN_INFO_UPDATED = 'plugin-info-updated'; const PDF_CONTENT_TYPE = 'application/pdf'; Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/Services.jsm'); let Svc = {}; XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', '@mozilla.org/mime;1', 'nsIMIMEService'); XPCOMUtils.defineLazyServiceGetter(Svc, 'pluginHost', '@mozilla.org/plugin/host;1', 'nsIPluginHost'); -XPCOMUtils.defineLazyModuleGetter(this, "PdfjsChromeUtils", - "resource://pdf.js/PdfjsChromeUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PdfjsContentUtils", - "resource://pdf.js/PdfjsContentUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsChromeUtils', + 'resource://pdf.js/PdfjsChromeUtils.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils', + 'resource://pdf.js/PdfjsContentUtils.jsm'); function getBoolPref(aPref, aDefaultValue) { try { return Services.prefs.getBoolPref(aPref); } catch (ex) { return aDefaultValue; } } @@ -59,17 +67,17 @@ function getIntPref(aPref, aDefaultValue try { return Services.prefs.getIntPref(aPref); } catch (ex) { return aDefaultValue; } } function isDefaultHandler() { - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { return PdfjsContentUtils.isDefaultHandlerApp(); } return PdfjsChromeUtils.isDefaultHandlerApp(); } function initializeDefaultPreferences() { var DEFAULT_PREFERENCES = { @@ -129,18 +137,20 @@ Factory.prototype = { }; let PdfJs = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), _registered: false, _initialized: false, init: function init(remote) { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - throw new Error("PdfJs.init should only get called in the parent process."); + if (Services.appinfo.processType !== + Services.appinfo.PROCESS_TYPE_DEFAULT) { + throw new Error('PdfJs.init should only get called ' + + 'in the parent process.'); } PdfjsChromeUtils.init(); if (!remote) { PdfjsContentUtils.init(); } this.initPrefs(); this.updateRegistration(); }, @@ -234,28 +244,29 @@ let PdfJs = { } if (types.indexOf(PDF_CONTENT_TYPE) === -1) { types.push(PDF_CONTENT_TYPE); } prefs.setCharPref(PREF_DISABLED_PLUGIN_TYPES, types.join(',')); // Update the category manager in case the plugins are already loaded. - let categoryManager = Cc["@mozilla.org/categorymanager;1"]; + let categoryManager = Cc['@mozilla.org/categorymanager;1']; categoryManager.getService(Ci.nsICategoryManager). - deleteCategoryEntry("Gecko-Content-Viewers", + deleteCategoryEntry('Gecko-Content-Viewers', PDF_CONTENT_TYPE, false); }, // nsIObserver observe: function observe(aSubject, aTopic, aData) { this.updateRegistration(); - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { - let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm"; + if (Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_DEFAULT) { + let jsm = 'resource://pdf.js/PdfjsChromeUtils.jsm'; let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils; PdfjsChromeUtils.notifyChildOfSettingsChange(); } }, /** * pdf.js is only enabled if it is both selected as the pdf viewer and if the * global switch enabling it is true. @@ -277,19 +288,19 @@ let PdfJs = { let disabledPluginTypes = Services.prefs.getCharPref(PREF_DISABLED_PLUGIN_TYPES).split(','); if (disabledPluginTypes.indexOf(PDF_CONTENT_TYPE) >= 0) { return true; } } // Check if there is an enabled pdf plugin. - // Note: this check is performed last because getPluginTags() triggers costly - // plugin list initialization (bug 881575) - let tags = Cc["@mozilla.org/plugin/host;1"]. + // Note: this check is performed last because getPluginTags() triggers + // costly plugin list initialization (bug 881575) + let tags = Cc['@mozilla.org/plugin/host;1']. getService(Ci.nsIPluginHost). getPluginTags(); let enabledPluginFound = tags.some(function(tag) { if (tag.disabled) { return false; } let mimeTypes = tag.getMimeTypes(); return mimeTypes.some(function(mimeType) { @@ -297,37 +308,37 @@ let PdfJs = { }); }); // Use pdf.js if pdf plugin is not present or disabled return !enabledPluginFound; }, _ensureRegistered: function _ensureRegistered() { - if (this._registered) + if (this._registered) { return; - + } this._pdfStreamConverterFactory = new Factory(); Cu.import('resource://pdf.js/PdfStreamConverter.jsm'); this._pdfStreamConverterFactory.register(PdfStreamConverter); this._pdfRedirectorFactory = new Factory(); Cu.import('resource://pdf.js/PdfRedirector.jsm'); this._pdfRedirectorFactory.register(PdfRedirector); Svc.pluginHost.registerPlayPreviewMimeType(PDF_CONTENT_TYPE, true, 'data:application/x-moz-playpreview-pdfjs;,'); this._registered = true; }, _ensureUnregistered: function _ensureUnregistered() { - if (!this._registered) + if (!this._registered) { return; - + } this._pdfStreamConverterFactory.unregister(); Cu.unload('resource://pdf.js/PdfStreamConverter.jsm'); delete this._pdfStreamConverterFactory; this._pdfRedirectorFactory.unregister(); Cu.unload('resource://pdf.js/PdfRedirector.jsm'); delete this._pdfRedirectorFactory;
--- a/browser/extensions/pdfjs/content/PdfJsTelemetry.jsm +++ b/browser/extensions/pdfjs/content/PdfJsTelemetry.jsm @@ -9,63 +9,64 @@ * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -/* jshint esnext:true */ +/* jshint esnext:true, maxlen: 100 */ +/* globals Components, Services */ 'use strict'; this.EXPORTED_SYMBOLS = ['PdfJsTelemetry']; const Cu = Components.utils; Cu.import('resource://gre/modules/Services.jsm'); this.PdfJsTelemetry = { onViewerIsUsed: function () { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_USED"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_USED'); histogram.add(true); }, onFallback: function () { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FALLBACK_SHOWN"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FALLBACK_SHOWN'); histogram.add(true); }, onDocumentSize: function (size) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_SIZE_KB"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_SIZE_KB'); histogram.add(size / 1024); }, onDocumentVersion: function (versionId) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_VERSION"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_VERSION'); histogram.add(versionId); }, onDocumentGenerator: function (generatorId) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_GENERATOR"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_GENERATOR'); histogram.add(generatorId); }, onEmbed: function (isObject) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_EMBED"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_EMBED'); histogram.add(isObject); }, onFontType: function (fontTypeId) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FONT_TYPES"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FONT_TYPES'); histogram.add(fontTypeId); }, onForm: function (isAcroform) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FORM"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FORM'); histogram.add(isAcroform); }, onPrint: function () { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_PRINT"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_PRINT'); histogram.add(true); }, onStreamType: function (streamTypeId) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_STREAM_TYPES"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_STREAM_TYPES'); histogram.add(streamTypeId); }, onTimeToView: function (ms) { - let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_TIME_TO_VIEW_MS"); + let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_TIME_TO_VIEW_MS'); histogram.add(ms); } };
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm +++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm @@ -11,17 +11,17 @@ * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* jshint esnext:true */ /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, - dump, NetworkManager, PdfJsTelemetry */ + dump, NetworkManager, PdfJsTelemetry, PdfjsContentUtils */ 'use strict'; var EXPORTED_SYMBOLS = ['PdfStreamConverter']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; @@ -138,30 +138,33 @@ function getLocalizedStrings(path) { while (enumerator.hasMoreElements()) { var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); 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)) + if (!(key in map)) { map[key] = {}; + } map[key][property] = string.value; } return map; } function getLocalizedString(strings, id, property) { property = property || 'textContent'; - if (id in strings) + if (id in strings) { return strings[id][property]; + } return id; } function makeContentReadable(obj, window) { + /* jshint -W027 */ return Cu.cloneInto(obj, window); } // PDF data storage function PdfDataListener(length) { this.length = length; // less than 0, if length is unknown this.buffer = null; this.loaded = 0; @@ -237,17 +240,17 @@ ChromeActions.prototype = { }, download: function(data, sendResponse) { var self = this; var originalUrl = data.originalUrl; // The data may not be downloaded so we need just retry getting the pdf with // the original url. var originalUri = NetUtil.newURI(data.originalUrl); var filename = data.filename; - if (typeof filename !== 'string' || + if (typeof filename !== 'string' || (!/\.pdf$/i.test(filename) && !data.isAttachment)) { filename = 'document.pdf'; } var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri; var extHelperAppSvc = Cc['@mozilla.org/uriloader/external-helper-app-service;1']. getService(Ci.nsIExternalHelperAppService); var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1']. @@ -256,18 +259,19 @@ ChromeActions.prototype = { var docIsPrivate = this.isInPrivateBrowsing(); var netChannel = NetUtil.newChannel(blobUri); if ('nsIPrivateBrowsingChannel' in Ci && netChannel instanceof Ci.nsIPrivateBrowsingChannel) { netChannel.setPrivate(docIsPrivate); } NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) { if (!Components.isSuccessCode(aResult)) { - if (sendResponse) + if (sendResponse) { sendResponse(true); + } return; } // Create a nsIInputStreamChannel so we can set the url on the channel // so the filename will be correct. var channel = Cc['@mozilla.org/network/input-stream-channel;1']. createInstance(Ci.nsIInputStreamChannel); channel.QueryInterface(Ci.nsIChannel); try { @@ -291,21 +295,23 @@ ChromeActions.prototype = { onStartRequest: function(aRequest, aContext) { this.extListener = extHelperAppSvc.doContent( (data.isAttachment ? 'application/octet-stream' : 'application/pdf'), aRequest, frontWindow, false); this.extListener.onStartRequest(aRequest, aContext); }, onStopRequest: function(aRequest, aContext, aStatusCode) { - if (this.extListener) + if (this.extListener) { this.extListener.onStopRequest(aRequest, aContext, aStatusCode); + } // Notify the content code we're done downloading. - if (sendResponse) + if (sendResponse) { sendResponse(false); + } }, onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { this.extListener.onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount); } }; @@ -313,19 +319,19 @@ ChromeActions.prototype = { }); }, getLocale: function() { return getStringPref('general.useragent.locale', 'en-US'); }, getStrings: function(data) { try { // Lazy initialization of localizedStrings - if (!('localizedStrings' in this)) + if (!('localizedStrings' in this)) { this.localizedStrings = getLocalizedStrings('viewer.properties'); - + } var result = this.localizedStrings[data]; return JSON.stringify(result || null); } catch (e) { log('Unable to retrive localized strings: ' + e); return 'null'; } }, supportsIntegratedFind: function() { @@ -368,31 +374,31 @@ ChromeActions.prototype = { case 'documentStats': // documentStats can be called several times for one documents. // if stream/font types are reported, trying not to submit the same // enumeration value multiple times. var documentStats = probeInfo.stats; if (!documentStats || typeof documentStats !== 'object') { break; } - var streamTypes = documentStats.streamTypes; + var i, streamTypes = documentStats.streamTypes; if (Array.isArray(streamTypes)) { var STREAM_TYPE_ID_LIMIT = 20; - for (var i = 0; i < STREAM_TYPE_ID_LIMIT; i++) { + for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) { if (streamTypes[i] && !this.telemetryState.streamTypesUsed[i]) { PdfJsTelemetry.onStreamType(i); this.telemetryState.streamTypesUsed[i] = true; } } } var fontTypes = documentStats.fontTypes; if (Array.isArray(fontTypes)) { var FONT_TYPE_ID_LIMIT = 20; - for (var i = 0; i < FONT_TYPE_ID_LIMIT; i++) { + for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) { if (fontTypes[i] && !this.telemetryState.fontTypesUsed[i]) { PdfJsTelemetry.onFontType(i); this.telemetryState.fontTypesUsed[i] = true; } } } break; @@ -415,18 +421,19 @@ ChromeActions.prototype = { message = getLocalizedString(strings, 'unsupported_feature'); } PdfJsTelemetry.onFallback(); PdfjsContentUtils.displayWarning(domWindow, message, sendResponse, getLocalizedString(strings, 'open_with_different_viewer'), getLocalizedString(strings, 'open_with_different_viewer', 'accessKey')); }, updateFindControlState: function(data) { - if (!this.supportsIntegratedFind()) + if (!this.supportsIntegratedFind()) { return; + } // Verify what we're sending to the findbar. var result = data.result; var findPrevious = data.findPrevious; var findPreviousType = typeof findPrevious; if ((typeof result !== 'number' || result < 0 || result > 3) || (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) { return; } @@ -701,29 +708,30 @@ RequestListener.prototype.receive = func var action = event.detail.action; var data = event.detail.data; var sync = event.detail.sync; var actions = this.actions; if (!(action in actions)) { log('Unknown action: ' + action); return; } + var response; if (sync) { - var response = actions[action].call(this.actions, data); + response = actions[action].call(this.actions, data); event.detail.response = response; } else { - var response; if (!event.detail.responseExpected) { doc.documentElement.removeChild(message); response = null; } else { response = function sendResponse(response) { try { var listener = doc.createEvent('CustomEvent'); - let detail = makeContentReadable({response: response}, doc.defaultView); + let detail = makeContentReadable({response: response}, + doc.defaultView); listener.initCustomEvent('pdf.js.response', true, false, detail); return message.dispatchEvent(listener); } catch (e) { // doc is no longer accessible because the requestor is already // gone. unloaded content cannot receive the response anyway. return false; } }; @@ -982,17 +990,18 @@ PdfStreamConverter.prototype = { // nsIRequestObserver::onStopRequest onStopRequest: function(aRequest, aContext, aStatusCode) { if (!this.dataListener) { // Do nothing return; } - if (Components.isSuccessCode(aStatusCode)) + if (Components.isSuccessCode(aStatusCode)) { this.dataListener.finish(); - else + } else { this.dataListener.error(aStatusCode); + } delete this.dataListener; delete this.binaryStream; } };
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm +++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm @@ -9,17 +9,18 @@ * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ - /*globals DEFAULT_PREFERENCES */ +/* jshint esnext:true */ +/* globals Components, Services, XPCOMUtils, DEFAULT_PREFERENCES */ 'use strict'; var EXPORTED_SYMBOLS = ['PdfjsChromeUtils']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; @@ -62,110 +63,113 @@ let PdfjsChromeUtils = { /* * Public API */ init: function () { if (!this._ppmm) { // global parent process message manager (PPMM) - this._ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); - this._ppmm.addMessageListener("PDFJS:Parent:clearUserPref", this); - this._ppmm.addMessageListener("PDFJS:Parent:setIntPref", this); - this._ppmm.addMessageListener("PDFJS:Parent:setBoolPref", this); - this._ppmm.addMessageListener("PDFJS:Parent:setCharPref", this); - this._ppmm.addMessageListener("PDFJS:Parent:setStringPref", this); - this._ppmm.addMessageListener("PDFJS:Parent:isDefaultHandlerApp", this); + this._ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1']. + getService(Ci.nsIMessageBroadcaster); + this._ppmm.addMessageListener('PDFJS:Parent:clearUserPref', this); + this._ppmm.addMessageListener('PDFJS:Parent:setIntPref', this); + this._ppmm.addMessageListener('PDFJS:Parent:setBoolPref', this); + this._ppmm.addMessageListener('PDFJS:Parent:setCharPref', this); + this._ppmm.addMessageListener('PDFJS:Parent:setStringPref', this); + this._ppmm.addMessageListener('PDFJS:Parent:isDefaultHandlerApp', this); // global dom message manager (MMg) - this._mmg = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); - this._mmg.addMessageListener("PDFJS:Parent:getChromeWindow", this); - this._mmg.addMessageListener("PDFJS:Parent:getFindBar", this); - this._mmg.addMessageListener("PDFJS:Parent:displayWarning", this); + this._mmg = Cc['@mozilla.org/globalmessagemanager;1']. + getService(Ci.nsIMessageListenerManager); + this._mmg.addMessageListener('PDFJS:Parent:getChromeWindow', this); + this._mmg.addMessageListener('PDFJS:Parent:getFindBar', this); + this._mmg.addMessageListener('PDFJS:Parent:displayWarning', this); // observer to handle shutdown - Services.obs.addObserver(this, "quit-application", false); + Services.obs.addObserver(this, 'quit-application', false); } }, uninit: function () { if (this._ppmm) { - this._ppmm.removeMessageListener("PDFJS:Parent:clearUserPref", this); - this._ppmm.removeMessageListener("PDFJS:Parent:setIntPref", this); - this._ppmm.removeMessageListener("PDFJS:Parent:setBoolPref", this); - this._ppmm.removeMessageListener("PDFJS:Parent:setCharPref", this); - this._ppmm.removeMessageListener("PDFJS:Parent:setStringPref", this); - this._ppmm.removeMessageListener("PDFJS:Parent:isDefaultHandlerApp", this); + this._ppmm.removeMessageListener('PDFJS:Parent:clearUserPref', this); + this._ppmm.removeMessageListener('PDFJS:Parent:setIntPref', this); + this._ppmm.removeMessageListener('PDFJS:Parent:setBoolPref', this); + this._ppmm.removeMessageListener('PDFJS:Parent:setCharPref', this); + this._ppmm.removeMessageListener('PDFJS:Parent:setStringPref', this); + this._ppmm.removeMessageListener('PDFJS:Parent:isDefaultHandlerApp', + this); - this._mmg.removeMessageListener("PDFJS:Parent:getChromeWindow", this); - this._mmg.removeMessageListener("PDFJS:Parent:getFindBar", this); - this._mmg.removeMessageListener("PDFJS:Parent:displayWarning", this); + this._mmg.removeMessageListener('PDFJS:Parent:getChromeWindow', this); + this._mmg.removeMessageListener('PDFJS:Parent:getFindBar', this); + this._mmg.removeMessageListener('PDFJS:Parent:displayWarning', this); - Services.obs.removeObserver(this, "quit-application", false); + Services.obs.removeObserver(this, 'quit-application', false); this._mmg = null; this._ppmm = null; } }, /* * Called by the main module when preference changes are picked up * in the parent process. Observers don't propagate so we need to * instruct the child to refresh its configuration and (possibly) * the module's registration. */ notifyChildOfSettingsChange: function () { - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT && - this._ppmm) { + if (Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_DEFAULT && this._ppmm) { // XXX kinda bad, we want to get the parent process mm associated // with the content process. _ppmm is currently the global process // manager, which means this is going to fire to every child process // we have open. Unfortunately I can't find a way to get at that // process specific mm from js. - this._ppmm.broadcastAsyncMessage("PDFJS:Child:refreshSettings", {}); + this._ppmm.broadcastAsyncMessage('PDFJS:Child:refreshSettings', {}); } }, /* * Events */ observe: function(aSubject, aTopic, aData) { - if (aTopic == "quit-application") { + if (aTopic === 'quit-application') { this.uninit(); } }, receiveMessage: function (aMsg) { switch (aMsg.name) { - case "PDFJS:Parent:clearUserPref": + case 'PDFJS:Parent:clearUserPref': this._clearUserPref(aMsg.data.name); break; - case "PDFJS:Parent:setIntPref": + case 'PDFJS:Parent:setIntPref': this._setIntPref(aMsg.data.name, aMsg.data.value); break; - case "PDFJS:Parent:setBoolPref": + case 'PDFJS:Parent:setBoolPref': this._setBoolPref(aMsg.data.name, aMsg.data.value); break; - case "PDFJS:Parent:setCharPref": + case 'PDFJS:Parent:setCharPref': this._setCharPref(aMsg.data.name, aMsg.data.value); break; - case "PDFJS:Parent:setStringPref": + case 'PDFJS:Parent:setStringPref': this._setStringPref(aMsg.data.name, aMsg.data.value); break; - case "PDFJS:Parent:isDefaultHandlerApp": + case 'PDFJS:Parent:isDefaultHandlerApp': return this.isDefaultHandlerApp(); - case "PDFJS:Parent:displayWarning": + case 'PDFJS:Parent:displayWarning': this._displayWarning(aMsg); break; // CPOW getters - case "PDFJS:Parent:getChromeWindow": + case 'PDFJS:Parent:getChromeWindow': return this._getChromeWindow(aMsg); - case "PDFJS:Parent:getFindBar": + case 'PDFJS:Parent:getFindBar': return this._getFindBar(aMsg); } }, /* * Internal */ @@ -188,18 +192,18 @@ let PdfjsChromeUtils = { suitcase.setFindBar(wrapper); return true; }, _ensurePreferenceAllowed: function (aPrefName) { let unPrefixedName = aPrefName.split(PREF_PREFIX + '.'); if (unPrefixedName[0] !== '' || this._allowedPrefNames.indexOf(unPrefixedName[1]) === -1) { - let msg = "'" + aPrefName + "' "; - msg += "can't be accessed from content. See PdfjsChromeUtils." + let msg = '"' + aPrefName + '" ' + + 'can\'t be accessed from content. See PdfjsChromeUtils.'; throw new Error(msg); } }, _clearUserPref: function (aPrefName) { this._ensurePreferenceAllowed(aPrefName); Services.prefs.clearUserPref(aPrefName); }, @@ -229,18 +233,18 @@ let PdfjsChromeUtils = { /* * Svc.mime doesn't have profile information in the child, so * we bounce this pdfjs enabled configuration check over to the * parent. */ isDefaultHandlerApp: function () { var handlerInfo = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf'); - return !handlerInfo.alwaysAskBeforeHandling && - handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally; + return (!handlerInfo.alwaysAskBeforeHandling && + handlerInfo.preferredAction === Ci.nsIHandlerInfo.handleInternally); }, /* * Display a notification warning when the renderer isn't sure * a pdf displayed correctly. */ _displayWarning: function (aMsg) { let json = aMsg.data; @@ -285,47 +289,47 @@ let PdfjsChromeUtils = { * directly on the findbar, so we wrap the findbar in a smaller * object here that supports the features pdf.js needs. */ function PdfjsFindbarWrapper(aBrowser) { let tabbrowser = aBrowser.getTabBrowser(); let tab; tab = tabbrowser.getTabForBrowser(aBrowser); this._findbar = tabbrowser.getFindBar(tab); -}; +} PdfjsFindbarWrapper.prototype = { __exposedProps__: { - addEventListener: "r", - removeEventListener: "r", - updateControlState: "r", + addEventListener: 'r', + removeEventListener: 'r', + updateControlState: 'r', }, _findbar: null, updateControlState: function (aResult, aFindPrevious) { this._findbar.updateControlState(aResult, aFindPrevious); }, addEventListener: function (aType, aListener, aUseCapture, aWantsUntrusted) { - this._findbar.addEventListener(aType, aListener, aUseCapture, aWantsUntrusted); + this._findbar.addEventListener(aType, aListener, aUseCapture, + aWantsUntrusted); }, removeEventListener: function (aType, aListener, aUseCapture) { this._findbar.removeEventListener(aType, aListener, aUseCapture); } }; function PdfjsWindowWrapper(aBrowser) { this._window = aBrowser.ownerDocument.defaultView; -}; +} PdfjsWindowWrapper.prototype = { __exposedProps__: { - valueOf: "r", + valueOf: 'r', }, _window: null, valueOf: function () { return this._window.valueOf(); } }; -
--- a/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm +++ b/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm @@ -1,22 +1,26 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ +/* jshint esnext:true */ +/* globals Components, Services, XPCOMUtils */ 'use strict'; var EXPORTED_SYMBOLS = ['PdfjsContentUtils']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; @@ -28,120 +32,123 @@ Cu.import('resource://gre/modules/Servic let PdfjsContentUtils = { _mm: null, /* * Public API */ get isRemote() { - return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + return (Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_CONTENT); }, init: function () { // child *process* mm, or when loaded into the parent for in-content // support the psuedo child process mm 'child PPMM'. if (!this._mm) { - this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); - this._mm.addMessageListener("PDFJS:Child:refreshSettings", this); - Services.obs.addObserver(this, "quit-application", false); + this._mm = Cc['@mozilla.org/childprocessmessagemanager;1']. + getService(Ci.nsISyncMessageSender); + this._mm.addMessageListener('PDFJS:Child:refreshSettings', this); + Services.obs.addObserver(this, 'quit-application', false); } }, uninit: function () { if (this._mm) { - this._mm.removeMessageListener("PDFJS:Child:refreshSettings", this); - Services.obs.removeObserver(this, "quit-application"); + this._mm.removeMessageListener('PDFJS:Child:refreshSettings', this); + Services.obs.removeObserver(this, 'quit-application'); } this._mm = null; }, /* * prefs utilities - the child does not have write access to prefs. * note, the pref names here are cross-checked against a list of * approved pdfjs prefs in chrome utils. */ clearUserPref: function (aPrefName) { - this._mm.sendSyncMessage("PDFJS:Parent:clearUserPref", { + this._mm.sendSyncMessage('PDFJS:Parent:clearUserPref', { name: aPrefName }); }, setIntPref: function (aPrefName, aPrefValue) { - this._mm.sendSyncMessage("PDFJS:Parent:setIntPref", { + this._mm.sendSyncMessage('PDFJS:Parent:setIntPref', { name: aPrefName, value: aPrefValue }); }, setBoolPref: function (aPrefName, aPrefValue) { - this._mm.sendSyncMessage("PDFJS:Parent:setBoolPref", { + this._mm.sendSyncMessage('PDFJS:Parent:setBoolPref', { name: aPrefName, value: aPrefValue }); }, setCharPref: function (aPrefName, aPrefValue) { - this._mm.sendSyncMessage("PDFJS:Parent:setCharPref", { + this._mm.sendSyncMessage('PDFJS:Parent:setCharPref', { name: aPrefName, value: aPrefValue }); }, setStringPref: function (aPrefName, aPrefValue) { - this._mm.sendSyncMessage("PDFJS:Parent:setStringPref", { + this._mm.sendSyncMessage('PDFJS:Parent:setStringPref', { name: aPrefName, value: aPrefValue }); }, /* * Forwards default app query to the parent where we check various * handler app settings only available in the parent process. */ isDefaultHandlerApp: function () { - return this._mm.sendSyncMessage("PDFJS:Parent:isDefaultHandlerApp")[0]; + return this._mm.sendSyncMessage('PDFJS:Parent:isDefaultHandlerApp')[0]; }, /* * Request the display of a notification warning in the associated window * when the renderer isn't sure a pdf displayed correctly. */ displayWarning: function (aWindow, aMessage, aCallback, aLabel, accessKey) { // the child's dom frame mm associated with the window. let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIContentFrameMessageManager); - winmm.sendAsyncMessage("PDFJS:Parent:displayWarning", { + winmm.sendAsyncMessage('PDFJS:Parent:displayWarning', { message: aMessage, label: aLabel, accessKey: accessKey }, { callback: aCallback }); }, /* * Events */ observe: function(aSubject, aTopic, aData) { - if (aTopic == "quit-application") { + if (aTopic === 'quit-application') { this.uninit(); } }, receiveMessage: function (aMsg) { switch (aMsg.name) { - case "PDFJS:Child:refreshSettings": + case 'PDFJS:Child:refreshSettings': // Only react to this if we are remote. - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - let jsm = "resource://pdf.js/PdfJs.jsm"; + if (Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_CONTENT) { + let jsm = 'resource://pdf.js/PdfJs.jsm'; let pdfjs = Components.utils.import(jsm, {}).PdfJs; pdfjs.updateRegistration(); } break; } }, /* @@ -154,40 +161,44 @@ let PdfjsContentUtils = { .sameTypeRootTreeItem .QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIContentFrameMessageManager); // Sync calls don't support cpow wrapping of returned results, so we // send over a small container for the object we want. let suitcase = { _window: null, - setChromeWindow: function (aObj) { this._window = aObj; } - } - if (!winmm.sendSyncMessage("PDFJS:Parent:getChromeWindow", {}, + setChromeWindow: function (aObj) { + this._window = aObj; + } + }; + if (!winmm.sendSyncMessage('PDFJS:Parent:getChromeWindow', {}, { suitcase: suitcase })[0]) { - Cu.reportError("A request for a CPOW wrapped chrome window " + - "failed for unknown reasons."); + Cu.reportError('A request for a CPOW wrapped chrome window ' + + 'failed for unknown reasons.'); return null; } return suitcase._window; }, getFindBar: function (aWindow) { let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell) .sameTypeRootTreeItem .QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIContentFrameMessageManager); let suitcase = { _findbar: null, - setFindBar: function (aObj) { this._findbar = aObj; } - } - if (!winmm.sendSyncMessage("PDFJS:Parent:getFindBar", {}, + setFindBar: function (aObj) { + this._findbar = aObj; + } + }; + if (!winmm.sendSyncMessage('PDFJS:Parent:getFindBar', {}, { suitcase: suitcase })[0]) { - Cu.reportError("A request for a CPOW wrapped findbar " + - "failed for unknown reasons."); + Cu.reportError('A request for a CPOW wrapped findbar ' + + 'failed for unknown reasons.'); return null; } return suitcase._findbar; } };
--- a/browser/extensions/pdfjs/content/build/pdf.js +++ b/browser/extensions/pdfjs/content/build/pdf.js @@ -17,18 +17,18 @@ /*jshint globalstrict: false */ /* globals PDFJS */ // Initializing PDFJS global object (if still undefined) if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.978'; -PDFJS.build = '20bf84a'; +PDFJS.version = '1.0.1040'; +PDFJS.build = '997096f'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it 'use strict'; /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation @@ -2798,17 +2798,23 @@ var Metadata = PDFJS.Metadata = (functio // However, PDF needs a bit more state, which we store here. // Minimal font size that would be used during canvas fillText operations. var MIN_FONT_SIZE = 16; // Maximum font size that would be used during canvas fillText operations. var MAX_FONT_SIZE = 100; var MAX_GROUP_SIZE = 4096; +// Heuristic value used when enforcing minimum line widths. +var MIN_WIDTH_FACTOR = 0.65; + var COMPILE_TYPE3_GLYPHS = true; +var MAX_SIZE_TO_COMPILE = 1000; + +var FULL_CHUNK_HEIGHT = 16; function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } @@ -2932,17 +2938,17 @@ function addContextCurrentTransform(ctx) } var CachedCanvases = (function CachedCanvasesClosure() { var cache = {}; return { getCanvas: function CachedCanvases_getCanvas(id, width, height, trackTransform) { var canvasEntry; - if (id in cache) { + if (cache[id] !== undefined) { canvasEntry = cache[id]; canvasEntry.canvas.width = width; canvasEntry.canvas.height = height; // reset canvas transform for emulated mozCurrentTransform, if needed canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0); } else { var canvas = createScratchCanvas(width, height); var ctx = canvas.getContext('2d'); @@ -3146,16 +3152,17 @@ var CanvasExtraState = (function CanvasE this.charSpacing = 0; this.wordSpacing = 0; this.textHScale = 1; this.textRenderingMode = TextRenderingMode.FILL; this.textRise = 0; // Default fore and background colors this.fillColor = '#000000'; this.strokeColor = '#000000'; + this.patternFill = false; // Note: fill alpha applies to all non-stroking operations this.fillAlpha = 1; this.strokeAlpha = 1; this.lineWidth = 1; this.activeSMask = null; // nonclonable field (see the save method below) this.old = old; } @@ -3198,16 +3205,17 @@ var CanvasGraphics = (function CanvasGra this.baseTransformStack = []; this.groupLevel = 0; this.smaskStack = []; this.smaskCounter = 0; this.tempSMask = null; if (canvasCtx) { addContextCurrentTransform(canvasCtx); } + this.cachedGetSinglePixelWidth = null; } function putBinaryImageData(ctx, imgData) { if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) { ctx.putImageData(imgData, 0, 0); return; } @@ -3218,23 +3226,21 @@ var CanvasGraphics = (function CanvasGra // the data passed to putImageData()). |n| shouldn't be too small, however, // because too many putImageData() calls will slow things down. // // Note: as written, if the last chunk is partial, the putImageData() call // will (conceptually) put pixels past the bounds of the canvas. But // that's ok; any such pixels are ignored. var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; - - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; + + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0, destPos; var src = imgData.data; var dest = chunkImgData.data; var i, j, thisChunkHeight, elemsInThisChunk; // There are multiple forms in which the pixel data can be passed, and // imgData.kind tells us which one this is. if (imgData.kind === ImageKind.GRAYSCALE_1BPP) { @@ -3244,17 +3250,17 @@ var CanvasGraphics = (function CanvasGra new Uint32ArrayView(dest); var dest32DataLength = dest32.length; var fullSrcDiff = (width + 7) >> 3; var white = 0xFFFFFFFF; var black = (PDFJS.isLittleEndian || !PDFJS.hasCanvasTypedArrays) ? 0xFF000000 : 0x000000FF; for (i = 0; i < totalChunks; i++) { thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; destPos = 0; for (j = 0; j < thisChunkHeight; j++) { var srcDiff = srcLength - srcPos; var k = 0; var kEnd = (srcDiff > fullSrcDiff) ? width : srcDiff * 8 - 7; var kEndUnrolled = kEnd & ~7; var mask = 0; var srcByte = 0; @@ -3279,110 +3285,108 @@ var CanvasGraphics = (function CanvasGra mask >>= 1; } } // We ran out of input. Make all remaining pixels transparent. while (destPos < dest32DataLength) { dest32[destPos++] = 0; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else if (imgData.kind === ImageKind.RGBA_32BPP) { // RGBA, 32-bits per pixel. j = 0; - elemsInThisChunk = width * fullChunkHeight * 4; + elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4; for (i = 0; i < fullChunks; i++) { dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); srcPos += elemsInThisChunk; ctx.putImageData(chunkImgData, 0, j); - j += fullChunkHeight; + j += FULL_CHUNK_HEIGHT; } if (i < totalChunks) { elemsInThisChunk = width * partialChunkHeight * 4; dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); ctx.putImageData(chunkImgData, 0, j); } } else if (imgData.kind === ImageKind.RGB_24BPP) { // RGB, 24-bits per pixel. - thisChunkHeight = fullChunkHeight; + thisChunkHeight = FULL_CHUNK_HEIGHT; elemsInThisChunk = width * thisChunkHeight; for (i = 0; i < totalChunks; i++) { if (i >= fullChunks) { - thisChunkHeight =partialChunkHeight; + thisChunkHeight = partialChunkHeight; elemsInThisChunk = width * thisChunkHeight; } destPos = 0; for (j = elemsInThisChunk; j--;) { dest[destPos++] = src[srcPos++]; dest[destPos++] = src[srcPos++]; dest[destPos++] = src[srcPos++]; dest[destPos++] = 255; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else { error('bad image kind: ' + imgData.kind); } } function putBinaryImageMask(ctx, imgData) { var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; - - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; + + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0; var src = imgData.data; var dest = chunkImgData.data; for (var i = 0; i < totalChunks; i++) { var thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; // Expand the mask so it can be used by the canvas. Any required // inversion has already been handled. var destPos = 3; // alpha component offset for (var j = 0; j < thisChunkHeight; j++) { var mask = 0; for (var k = 0; k < width; k++) { if (!mask) { var elem = src[srcPos++]; mask = 128; } dest[destPos] = (elem & mask) ? 0 : 255; destPos += 4; mask >>= 1; } } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } function copyCtxState(sourceCtx, destCtx) { var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha', 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'globalCompositeOperation', 'font']; for (var i = 0, ii = properties.length; i < ii; i++) { var property = properties[i]; - if (property in sourceCtx) { + if (sourceCtx[property] !== undefined) { destCtx[property] = sourceCtx[property]; } } - if ('setLineDash' in sourceCtx) { + if (sourceCtx.setLineDash !== undefined) { destCtx.setLineDash(sourceCtx.getLineDash()); destCtx.lineDashOffset = sourceCtx.lineDashOffset; - } else if ('mozDash' in sourceCtx) { + } else if (sourceCtx.mozDashOffset !== undefined) { destCtx.mozDash = sourceCtx.mozDash; destCtx.mozDashOffset = sourceCtx.mozDashOffset; } } function composeSMaskBackdrop(bytes, r0, g0, b0) { var length = bytes.length; for (var i = 3; i < length; i += 4) { @@ -3599,17 +3603,17 @@ var CanvasGraphics = (function CanvasGra setLineJoin: function CanvasGraphics_setLineJoin(style) { this.ctx.lineJoin = LINE_JOIN_STYLES[style]; }, setMiterLimit: function CanvasGraphics_setMiterLimit(limit) { this.ctx.miterLimit = limit; }, setDash: function CanvasGraphics_setDash(dashArray, dashPhase) { var ctx = this.ctx; - if ('setLineDash' in ctx) { + if (ctx.setLineDash !== undefined) { ctx.setLineDash(dashArray); ctx.lineDashOffset = dashPhase; } else { ctx.mozDash = dashArray; ctx.mozDashOffset = dashPhase; } }, setRenderingIntent: function CanvasGraphics_setRenderingIntent(intent) { @@ -3734,20 +3738,24 @@ var CanvasGraphics = (function CanvasGra restore: function CanvasGraphics_restore() { if (this.stateStack.length !== 0) { if (this.current.activeSMask !== null) { this.endSMaskGroup(); } this.current = this.stateStack.pop(); this.ctx.restore(); + + this.cachedGetSinglePixelWidth = null; } }, transform: function CanvasGraphics_transform(a, b, c, d, e, f) { this.ctx.transform(a, b, c, d, e, f); + + this.cachedGetSinglePixelWidth = null; }, // Path constructPath: function CanvasGraphics_constructPath(ops, args) { var ctx = this.ctx; var current = this.current; var x = current.x, y = current.y; for (var i = 0, j = 0, ii = ops.length; i < ii; i++) { @@ -3811,19 +3819,19 @@ var CanvasGraphics = (function CanvasGra }, closePath: function CanvasGraphics_closePath() { this.ctx.closePath(); }, stroke: function CanvasGraphics_stroke(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var strokeColor = this.current.strokeColor; - if (this.current.lineWidth === 0) { - ctx.lineWidth = this.getSinglePixelWidth(); - } + // Prevent drawing too thin lines by enforcing a minimum line width. + ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, + this.current.lineWidth); // For stroke we want to temporarily change the global alpha to the // stroking alpha. ctx.globalAlpha = this.current.strokeAlpha; if (strokeColor && strokeColor.hasOwnProperty('type') && strokeColor.type === 'Pattern') { // for patterns, we transform to pattern space, calculate // the pattern, call stroke, and restore to user space ctx.save(); @@ -3842,20 +3850,20 @@ var CanvasGraphics = (function CanvasGra closeStroke: function CanvasGraphics_closeStroke() { this.closePath(); this.stroke(); }, fill: function CanvasGraphics_fill(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var needRestore = false; - if (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') { + if (isPatternFill) { ctx.save(); ctx.fillStyle = fillColor.getPattern(ctx, this); needRestore = true; } if (this.pendingEOFill) { if (ctx.mozFillRule !== undefined) { ctx.mozFillRule = 'evenodd'; @@ -4138,17 +4146,23 @@ var CanvasGraphics = (function CanvasGra ctx.scale(textHScale, -1); } else { ctx.scale(textHScale, 1); } var lineWidth = current.lineWidth; var scale = current.textMatrixScale; if (scale === 0 || lineWidth === 0) { - lineWidth = this.getSinglePixelWidth(); + var fillStrokeMode = current.textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + this.cachedGetSinglePixelWidth = null; + lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; + } } else { lineWidth /= scale; } if (fontSizeScale !== 1.0) { ctx.scale(fontSizeScale, fontSizeScale); lineWidth /= fontSizeScale; } @@ -4319,26 +4333,28 @@ var CanvasGraphics = (function CanvasGra } return pattern; }, setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) { this.current.strokeColor = this.getColorN_Pattern(arguments); }, setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) { this.current.fillColor = this.getColorN_Pattern(arguments); + this.current.patternFill = true; }, setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) { var color = Util.makeCssRgb(r, g, b); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) { var color = Util.makeCssRgb(r, g, b); this.ctx.fillStyle = color; this.current.fillColor = color; + this.current.patternFill = false; }, shadingFill: function CanvasGraphics_shadingFill(patternIR) { var ctx = this.ctx; this.save(); var pattern = getShadingPatternFromIR(patternIR); ctx.fillStyle = pattern.getPattern(ctx, this, true); @@ -4587,21 +4603,22 @@ var CanvasGraphics = (function CanvasGra }); } this.restore(); }, paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { var ctx = this.ctx; var width = img.width, height = img.height; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var glyph = this.processingType3; - if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) { - var MAX_SIZE_TO_COMPILE = 1000; + if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) { if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) { glyph.compiled = compileType3Glyph({data: img.data, width: width, height: height}); } else { glyph.compiled = null; } } @@ -4613,79 +4630,77 @@ var CanvasGraphics = (function CanvasGra var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, img); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); this.paintInlineImageXObject(maskCanvas.canvas); }, paintImageMaskXObjectRepeat: function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, scaleY, positions) { var width = imgData.width; var height = imgData.height; - var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, imgData); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? - fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); + var ctx = this.ctx; for (var i = 0, ii = positions.length; i < ii; i += 2) { ctx.save(); ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]); ctx.scale(1, -1); ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); ctx.restore(); } }, paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) { var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; for (var i = 0, ii = images.length; i < ii; i++) { var image = images[i]; var width = image.width, height = image.height; var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, image); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); ctx.save(); ctx.transform.apply(ctx, image.transform); ctx.scale(1, -1); @@ -4878,21 +4893,24 @@ var CanvasGraphics = (function CanvasGra } else { ctx.clip(); } this.pendingClip = null; } ctx.beginPath(); }, getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) { - var inverse = this.ctx.mozCurrentTransformInverse; - // max of the current horizontal and vertical scale - return Math.sqrt(Math.max( - (inverse[0] * inverse[0] + inverse[1] * inverse[1]), - (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + if (this.cachedGetSinglePixelWidth === null) { + var inverse = this.ctx.mozCurrentTransformInverse; + // max of the current horizontal and vertical scale + this.cachedGetSinglePixelWidth = Math.sqrt(Math.max( + (inverse[0] * inverse[0] + inverse[1] * inverse[1]), + (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + } + return this.cachedGetSinglePixelWidth; }, getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { var transform = this.ctx.mozCurrentTransform; return [ transform[0] * x + transform[2] * y + transform[4], transform[1] * x + transform[3] * y + transform[5] ]; } @@ -5819,17 +5837,16 @@ var FontFaceObject = (function FontFaceO } return this.compiledGlyphs[character]; } }; return FontFaceObject; })(); -var HIGHLIGHT_OFFSET = 4; // px var ANNOT_MIN_SIZE = 10; // px var AnnotationUtils = (function AnnotationUtilsClosure() { // TODO(mack): This dupes some of the logic in CanvasGraphics.setFont() function setTextStyles(element, item, fontObj) { var style = element.style; style.fontSize = item.fontSize + 'px'; @@ -5846,51 +5863,46 @@ var AnnotationUtils = (function Annotati var fontName = fontObj.loadedName; var fontFamily = fontName ? '"' + fontName + '", ' : ''; // Use a reasonable default font if the font doesn't specify a fallback var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif'; style.fontFamily = fontFamily + fallbackName; } - // TODO(mack): Remove this, it's not really that helpful. - function getEmptyContainer(tagName, rect, borderWidth) { - var bWidth = borderWidth || 0; - var element = document.createElement(tagName); - element.style.borderWidth = bWidth + 'px'; - var width = rect[2] - rect[0] - 2 * bWidth; - var height = rect[3] - rect[1] - 2 * bWidth; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - } - - function initContainer(item) { - var container = getEmptyContainer('section', item.rect, item.borderWidth); - container.style.backgroundColor = item.color; - - var color = item.color; - item.colorCssRgb = Util.makeCssRgb(Math.round(color[0] * 255), - Math.round(color[1] * 255), - Math.round(color[2] * 255)); - - var highlight = document.createElement('div'); - highlight.className = 'annotationHighlight'; - highlight.style.left = highlight.style.top = -HIGHLIGHT_OFFSET + 'px'; - highlight.style.right = highlight.style.bottom = -HIGHLIGHT_OFFSET + 'px'; - highlight.setAttribute('hidden', true); - - item.highlightElement = highlight; - container.appendChild(item.highlightElement); - + function initContainer(item, drawBorder) { + var container = document.createElement('section'); + var cstyle = container.style; + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + + var bWidth = item.borderWidth || 0; + if (bWidth) { + width = width - 2 * bWidth; + height = height - 2 * bWidth; + cstyle.borderWidth = bWidth + 'px'; + var color = item.color; + if (drawBorder && color) { + cstyle.borderStyle = 'solid'; + cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255), + Math.round(color[1] * 255), + Math.round(color[2] * 255)); + } + } + cstyle.width = width + 'px'; + cstyle.height = height + 'px'; return container; } function getHtmlElementForTextWidgetAnnotation(item, commonObjs) { - var element = getEmptyContainer('div', item.rect, 0); + var element = document.createElement('div'); + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + element.style.width = width + 'px'; + element.style.height = height + 'px'; element.style.display = 'table'; var content = document.createElement('div'); content.textContent = item.fieldValue; var textAlignment = item.textAlignment; content.style.textAlign = ['left', 'center', 'right'][textAlignment]; content.style.verticalAlign = 'middle'; content.style.display = 'table-cell'; @@ -5910,17 +5922,17 @@ var AnnotationUtils = (function Annotati // sanity check because of OOo-generated PDFs if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) { rect[3] = rect[1] + ANNOT_MIN_SIZE; } if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) { rect[2] = rect[0] + (rect[3] - rect[1]); // make it square } - var container = initContainer(item); + var container = initContainer(item, false); container.className = 'annotText'; var image = document.createElement('img'); image.style.height = container.style.height; image.style.width = container.style.width; var iconName = item.name; image.src = PDFJS.imageResourcesPath + 'annotation-' + iconName.toLowerCase() + '.svg'; @@ -6019,22 +6031,19 @@ var AnnotationUtils = (function Annotati contentWrapper.appendChild(content); container.appendChild(image); container.appendChild(contentWrapper); return container; } function getHtmlElementForLinkAnnotation(item) { - var container = initContainer(item); + var container = initContainer(item, true); container.className = 'annotLink'; - container.style.borderColor = item.colorCssRgb; - container.style.borderStyle = 'solid'; - var link = document.createElement('a'); link.href = link.title = item.url || ''; container.appendChild(link); return container; }
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js +++ b/browser/extensions/pdfjs/content/build/pdf.worker.js @@ -17,18 +17,18 @@ /*jshint globalstrict: false */ /* globals PDFJS */ // Initializing PDFJS global object (if still undefined) if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.978'; -PDFJS.build = '20bf84a'; +PDFJS.version = '1.0.1040'; +PDFJS.build = '997096f'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it 'use strict'; /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation @@ -2293,16 +2293,20 @@ var Page = (function PageClosure() { /** * The `PDFDocument` holds all the data of the PDF file. Compared to the * `PDFDoc`, this one doesn't have any job management code. * Right now there exists one PDFDocument on the main thread + one object * for each worker. If there is no worker support enabled, there are two * `PDFDocument` objects on the main thread created. */ var PDFDocument = (function PDFDocumentClosure() { + var FINGERPRINT_FIRST_BYTES = 1024; + var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' + + '\x00\x00\x00\x00\x00\x00\x00\x00\x00'; + function PDFDocument(pdfManager, arg, password) { if (isStream(arg)) { init.call(this, pdfManager, arg, password); } else if (isArrayBuffer(arg)) { init.call(this, pdfManager, new Stream(arg), password); } else { error('PDFDocument: Unknown argument type'); } @@ -2504,26 +2508,35 @@ var PDFDocument = (function PDFDocumentC info('Bad value in document info for "' + key + '"'); } } } } return shadow(this, 'documentInfo', docInfo); }, get fingerprint() { - var xref = this.xref, hash, fileID = ''; + var xref = this.xref, idArray, hash, fileID = ''; if (xref.trailer.has('ID')) { - hash = stringToBytes(xref.trailer.get('ID')[0]); - } else { - hash = calculateMD5(this.stream.bytes.subarray(0, 100), 0, 100); + idArray = xref.trailer.get('ID'); + } + if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) { + hash = stringToBytes(idArray[0]); + } else { + if (this.stream.ensureRange) { + this.stream.ensureRange(0, + Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end)); + } + hash = calculateMD5(this.stream.bytes.subarray(0, + FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES); } for (var i = 0, n = hash.length; i < n; i++) { - fileID += hash[i].toString(16); + var hex = hash[i].toString(16); + fileID += hex.length === 1 ? '0' + hex : hex; } return shadow(this, 'fingerprint', fileID); }, getPage: function PDFDocument_getPage(pageIndex) { return this.catalog.getPage(pageIndex); }, @@ -4369,39 +4382,53 @@ var Annotation = (function AnnotationClo var data = this.data = {}; data.subtype = dict.get('Subtype').name; var rect = dict.get('Rect') || [0, 0, 0, 0]; data.rect = Util.normalizeRect(rect); data.annotationFlags = dict.get('F'); var color = dict.get('C'); - if (isArray(color) && color.length === 3) { - // TODO(mack): currently only supporting rgb; need support different - // colorspaces - data.color = color; - } else { + if (!color) { + // The PDF spec does not mention how a missing color array is interpreted. + // Adobe Reader seems to default to black in this case. data.color = [0, 0, 0]; + } else if (isArray(color)) { + switch (color.length) { + case 0: + // Empty array denotes transparent border. + data.color = null; + break; + case 1: + // TODO: implement DeviceGray + break; + case 3: + data.color = color; + break; + case 4: + // TODO: implement DeviceCMYK + break; + } } // Some types of annotations have border style dict which has more // info than the border array if (dict.has('BS')) { var borderStyle = dict.get('BS'); data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1; } else { var borderArray = dict.get('Border') || [0, 0, 1]; data.borderWidth = borderArray[2] || 0; // TODO: implement proper support for annotations with line dash patterns. var dashArray = borderArray[3]; if (data.borderWidth > 0 && dashArray) { if (!isArray(dashArray)) { // Ignore the border if dashArray is not actually an array, - // this is consistent with the behaviour in Adobe Reader. + // this is consistent with the behaviour in Adobe Reader. data.borderWidth = 0; } else { var dashArrayLength = dashArray.length; if (dashArrayLength > 0) { // According to the PDF specification: the elements in a dashArray // shall be numbers that are nonnegative and not all equal to zero. var isInvalid = false; var numPositive = 0; @@ -4814,21 +4841,17 @@ var LinkAnnotation = (function LinkAnnot // Lets URLs beginning with 'www.' default to using the 'http://' protocol. function addDefaultProtocolToUrl(url) { if (url && url.indexOf('www.') === 0) { return ('http://' + url); } return url; } - Util.inherit(LinkAnnotation, InteractiveAnnotation, { - hasOperatorList: function LinkAnnotation_hasOperatorList() { - return false; - } - }); + Util.inherit(LinkAnnotation, InteractiveAnnotation, { }); return LinkAnnotation; })(); var PDFFunction = (function PDFFunctionClosure() { var CONSTRUCT_SAMPLED = 0; var CONSTRUCT_INTERPOLATED = 2; @@ -10115,17 +10138,17 @@ var PartialEvaluator = (function Partial operatorList.addOp(OPS.endGroup, [groupOptions]); } }); }, buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject(resources, image, inline, operatorList, - cacheKey, cache) { + cacheKey, imageCache) { var self = this; var dict = image.dict; var w = dict.get('Width', 'W'); var h = dict.get('Height', 'H'); if (!(w && isNum(w)) || !(h && isNum(h))) { warn('Image dimensions are missing, or not numbers.'); return; @@ -10153,19 +10176,20 @@ var PartialEvaluator = (function Partial imgData = PDFImage.createMask(imgArray, width, height, image instanceof DecodeStream, inverseDecode); imgData.cached = true; args = [imgData]; operatorList.addOp(OPS.paintImageMaskXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageMaskXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageMaskXObject, + args: args + }; } return; } var softMask = (dict.get('SMask', 'SM') || false); var mask = (dict.get('Mask') || false); var SMALL_IMAGE_DIMENSIONS = 200; @@ -10204,19 +10228,20 @@ var PartialEvaluator = (function Partial [imgData.data.buffer]); }).then(undefined, function (reason) { warn('Unable to decode image: ' + reason); self.handler.send('obj', [objId, self.pageIndex, 'Image', null]); }); operatorList.addOp(OPS.paintImageXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageXObject, + args: args + }; } }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, operatorList, stateManager) { var smaskContent = smask.get('G'); var smaskOptions = { @@ -10600,18 +10625,18 @@ var PartialEvaluator = (function Partial switch (fn | 0) { case OPS.paintXObject: if (args[0].code) { break; } // eagerly compile XForm objects var name = args[0].name; - if (imageCache.key === name) { - operatorList.addOp(imageCache.fn, imageCache.args); + if (imageCache[name] !== undefined) { + operatorList.addOp(imageCache[name].fn, imageCache[name].args); args = null; continue; } var xobj = xobjs.get(name); if (xobj) { assert(isStream(xobj), 'XObject should be a stream'); @@ -10650,20 +10675,23 @@ var PartialEvaluator = (function Partial operatorList, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); operatorList.addOp(OPS.setFont, [loadedName, fontSize]); next(resolve, reject); }, reject); case OPS.endInlineImage: var cacheKey = args[0].cacheKey; - if (cacheKey && imageCache.key === cacheKey) { - operatorList.addOp(imageCache.fn, imageCache.args); - args = null; - continue; + if (cacheKey) { + var cacheEntry = imageCache[cacheKey]; + if (cacheEntry !== undefined) { + operatorList.addOp(cacheEntry.fn, cacheEntry.args); + args = null; + continue; + } } self.buildPaintImageXObject(resources, args[0], true, operatorList, cacheKey, imageCache); args = null; continue; case OPS.showText: args[0] = self.handleText(args[0], stateManager.state); break; @@ -13820,19 +13848,22 @@ var stdFontMap = { 'CourierNew': 'Courier', 'CourierNew-Bold': 'Courier-Bold', 'CourierNew-BoldItalic': 'Courier-BoldOblique', 'CourierNew-Italic': 'Courier-Oblique', 'CourierNewPS-BoldItalicMT': 'Courier-BoldOblique', 'CourierNewPS-BoldMT': 'Courier-Bold', 'CourierNewPS-ItalicMT': 'Courier-Oblique', 'CourierNewPSMT': 'Courier', + 'Helvetica': 'Helvetica', 'Helvetica-Bold': 'Helvetica-Bold', 'Helvetica-BoldItalic': 'Helvetica-BoldOblique', + 'Helvetica-BoldOblique': 'Helvetica-BoldOblique', 'Helvetica-Italic': 'Helvetica-Oblique', + 'Helvetica-Oblique':'Helvetica-Oblique', 'Symbol-Bold': 'Symbol', 'Symbol-BoldItalic': 'Symbol', 'Symbol-Italic': 'Symbol', 'TimesNewRoman': 'Times-Roman', 'TimesNewRoman-Bold': 'Times-Bold', 'TimesNewRoman-BoldItalic': 'Times-BoldItalic', 'TimesNewRoman-Italic': 'Times-Italic', 'TimesNewRomanPS': 'Times-Roman', @@ -13848,16 +13879,20 @@ var stdFontMap = { 'TimesNewRomanPSMT-Italic': 'Times-Italic' }; /** * Holds the map of the non-standard fonts that might be included as a standard * fonts without glyph data. */ var nonStdFontMap = { + 'CenturyGothic': 'Helvetica', + 'CenturyGothic-Bold': 'Helvetica-Bold', + 'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique', + 'CenturyGothic-Italic': 'Helvetica-Oblique', 'ComicSansMS': 'Comic Sans MS', 'ComicSansMS-Bold': 'Comic Sans MS-Bold', 'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic', 'ComicSansMS-Italic': 'Comic Sans MS-Italic', 'LucidaConsole': 'Courier', 'LucidaConsole-Bold': 'Courier-Bold', 'LucidaConsole-BoldItalic': 'Courier-BoldOblique', 'LucidaConsole-Italic': 'Courier-Oblique', @@ -13871,17 +13906,18 @@ var nonStdFontMap = { 'MS-Mincho-Italic': 'MS Mincho-Italic', 'MS-PGothic': 'MS PGothic', 'MS-PGothic-Bold': 'MS PGothic-Bold', 'MS-PGothic-BoldItalic': 'MS PGothic-BoldItalic', 'MS-PGothic-Italic': 'MS PGothic-Italic', 'MS-PMincho': 'MS PMincho', 'MS-PMincho-Bold': 'MS PMincho-Bold', 'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic', - 'MS-PMincho-Italic': 'MS PMincho-Italic' + 'MS-PMincho-Italic': 'MS PMincho-Italic', + 'Wingdings': 'ZapfDingbats' }; var serifFonts = { 'Adobe Jenson': true, 'Adobe Text': true, 'Albertus': true, 'Aldus': true, 'Alexandria': true, 'Algerian': true, 'American Typewriter': true, 'Antiqua': true, 'Apex': true, 'Arno': true, 'Aster': true, 'Aurora': true, 'Baskerville': true, 'Bell': true, 'Bembo': true, @@ -15954,17 +15990,18 @@ var Font = (function FontClosure() { // attempting to recover by assuming that no file exists. warn('Font file is empty in "' + name + '" (' + this.loadedName + ')'); } this.missingFile = true; // The file data is not specified. Trying to fix the font name // to be used with the canvas.font. var fontName = name.replace(/[,_]/g, '-'); - var isStandardFont = fontName in stdFontMap; + var isStandardFont = !!stdFontMap[fontName] || + (nonStdFontMap[fontName] && !!stdFontMap[nonStdFontMap[fontName]]); fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName; this.bold = (fontName.search(/bold/gi) !== -1); this.italic = ((fontName.search(/oblique/gi) !== -1) || (fontName.search(/italic/gi) !== -1)); // Use 'name' instead of 'fontName' here because the original // name ArialBlack for example will be replaced by Helvetica. @@ -16000,16 +16037,20 @@ var Font = (function FontClosure() { for (charCode in properties.differences) { fontChar = GlyphsUnicode[properties.differences[charCode]]; if (!fontChar) { continue; } this.toFontChar[charCode] = fontChar; } } else if (/Dingbats/i.test(fontName)) { + if (/Wingdings/i.test(name)) { + warn('Wingdings font without embedded font file, ' + + 'falling back to the ZapfDingbats encoding.'); + } var dingbats = Encodings.ZapfDingbatsEncoding; for (charCode in dingbats) { fontChar = DingbatsGlyphsUnicode[dingbats[charCode]]; if (!fontChar) { continue; } this.toFontChar[charCode] = fontChar; } @@ -16185,16 +16226,17 @@ var Font = (function FontClosure() { // canvas if left in their current position. Also, move characters if the // font was symbolic and there is only an identity unicode map since the // characters probably aren't in the correct position (fixes an issue // with firefox and thuluthfont). if ((usedFontCharCodes[fontCharCode] !== undefined || fontCharCode <= 0x1f || // Control chars fontCharCode === 0x7F || // Control char fontCharCode === 0xAD || // Soft hyphen + fontCharCode === 0xA0 || // Non breaking space (fontCharCode >= 0x80 && fontCharCode <= 0x9F) || // Control chars // Prevent drawing characters in the specials unicode block. (fontCharCode >= 0xFFF0 && fontCharCode <= 0xFFFF) || (isSymbolic && isIdentityUnicode)) && nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END) { // Room left. // Loop to try and find a free spot in the private use area. do { fontCharCode = nextAvailableFontCharCode++; @@ -18203,44 +18245,50 @@ function type1FontGlyphMapping(propertie if (properties.baseEncodingName) { // If a valid base encoding name was used, the mapping is initialized with // that. baseEncoding = Encodings[properties.baseEncodingName]; for (charCode = 0; charCode < baseEncoding.length; charCode++) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } else if (!!(properties.flags & FontFlags.Symbolic)) { // For a symbolic font the encoding should be the fonts built-in // encoding. for (charCode in builtInEncoding) { charCodeToGlyphId[charCode] = builtInEncoding[charCode]; } } else { // For non-symbolic fonts that don't have a base encoding the standard // encoding should be used. baseEncoding = Encodings.StandardEncoding; for (charCode = 0; charCode < baseEncoding.length; charCode++) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } // Lastly, merge in the differences. var differences = properties.differences; if (differences) { for (charCode in differences) { var glyphName = differences[charCode]; glyphId = glyphNames.indexOf(glyphName); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } return charCodeToGlyphId; } /* * CharStrings are encoded following the the CharString Encoding sequence @@ -29406,26 +29454,24 @@ var Metrics = { var EOF = {}; function isEOF(v) { return (v === EOF); } +var MAX_LENGTH_TO_CACHE = 1000; + var Parser = (function ParserClosure() { function Parser(lexer, allowStreams, xref) { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; - this.imageCache = { - length: 0, - adler32: 0, - stream: null - }; + this.imageCache = {}; this.refill(); } Parser.prototype = { refill: function Parser_refill() { this.buf1 = this.lexer.getObj(); this.buf2 = this.lexer.getObj(); }, @@ -29506,113 +29552,188 @@ var Parser = (function ParserClosure() { str = cipherTransform.decryptString(str); } return str; } // simple object return buf1; }, - makeInlineImage: function Parser_makeInlineImage(cipherTransform) { - var lexer = this.lexer; - var stream = lexer.stream; - - // parse dictionary - var dict = new Dict(null); - while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { - if (!isName(this.buf1)) { - error('Dictionary key must be a name object'); - } - - var key = this.buf1.name; - this.shift(); - if (isEOF(this.buf1)) { - break; - } - dict.set(key, this.getObj(cipherTransform)); - } - - // parse image stream - var startPos = stream.pos; - - // searching for the /EI\s/ - var state = 0, ch, i, ii; - var E = 0x45, I = 0x49, SPACE = 0x20, NL = 0xA, CR = 0xD; + /** + * Find the end of the stream by searching for the /EI\s/. + * @returns {number} The inline stream length. + */ + findDefaultInlineStreamEnd: + function Parser_findDefaultInlineStreamEnd(stream) { + var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD; + var startPos = stream.pos, state = 0, ch, i, n, followingBytes; while ((ch = stream.getByte()) !== -1) { if (state === 0) { state = (ch === E) ? 1 : 0; } else if (state === 1) { state = (ch === I) ? 2 : 0; } else { assert(state === 2); - if (ch === SPACE || ch === NL || ch === CR) { + if (ch === SPACE || ch === LF || ch === CR) { // Let's check the next five bytes are ASCII... just be sure. - var n = 5; - var followingBytes = stream.peekBytes(n); + n = 5; + followingBytes = stream.peekBytes(n); for (i = 0; i < n; i++) { ch = followingBytes[i]; - if (ch !== NL && ch !== CR && (ch < SPACE || ch > 0x7F)) { + if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) { // Not a LF, CR, SPACE or any visible ASCII character, i.e. // it's binary stuff. Resetting the state. state = 0; break; } } if (state === 2) { - break; // finished! + break; // Finished! } } else { state = 0; } } } - - var length = (stream.pos - 4) - startPos; + return ((stream.pos - 4) - startPos); + }, + /** + * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCII85DecodeInlineStreamEnd: + function Parser_findASCII85DecodeInlineStreamEnd(stream) { + var TILDE = 0x7E, GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === TILDE && stream.peekByte() === GT) { + stream.skip(); + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCII85Decode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCIIHexDecodeInlineStreamEnd: + function Parser_findASCIIHexDecodeInlineStreamEnd(stream) { + var GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === GT) { + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCIIHexDecode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Skip over the /EI/ for streams where we search for an EOD marker. + */ + inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) { + var E = 0x45, I = 0x49; + var state = 0, ch; + while ((ch = stream.getByte()) !== -1) { + if (state === 0) { + state = (ch === E) ? 1 : 0; + } else if (state === 1) { + state = (ch === I) ? 2 : 0; + } else if (state === 2) { + break; + } + } + }, + makeInlineImage: function Parser_makeInlineImage(cipherTransform) { + var lexer = this.lexer; + var stream = lexer.stream; + + // Parse dictionary. + var dict = new Dict(null); + while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { + if (!isName(this.buf1)) { + error('Dictionary key must be a name object'); + } + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) { + break; + } + dict.set(key, this.getObj(cipherTransform)); + } + + // Extract the name of the first (i.e. the current) image filter. + var filter = this.fetchIfRef(dict.get('Filter', 'F')), filterName; + if (isName(filter)) { + filterName = filter.name; + } else if (isArray(filter) && isName(filter[0])) { + filterName = filter[0].name; + } + + // Parse image stream. + var startPos = stream.pos, length, i, ii; + if (filterName === 'ASCII85Decide' || filterName === 'A85') { + length = this.findASCII85DecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') { + length = this.findASCIIHexDecodeInlineStreamEnd(stream); + } else { + length = this.findDefaultInlineStreamEnd(stream); + } var imageStream = stream.makeSubStream(startPos, length, dict); - // trying to cache repeat images, first we are trying to "warm up" caching - // using length, then comparing adler32 - var MAX_LENGTH_TO_CACHE = 1000; - var cacheImage = false, adler32; - if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) { + // Cache all images below the MAX_LENGTH_TO_CACHE threshold by their + // adler32 checksum. + var adler32; + if (length < MAX_LENGTH_TO_CACHE) { var imageBytes = imageStream.getBytes(); imageStream.reset(); var a = 1; var b = 0; for (i = 0, ii = imageBytes.length; i < ii; ++i) { - a = (a + (imageBytes[i] & 0xff)) % 65521; - b = (b + a) % 65521; - } - adler32 = (b << 16) | a; - - if (this.imageCache.stream && this.imageCache.adler32 === adler32) { + // No modulo required in the loop if imageBytes.length < 5552. + a += imageBytes[i] & 0xff; + b += a; + } + adler32 = ((b % 65521) << 16) | (a % 65521); + + if (this.imageCache.adler32 === adler32) { this.buf2 = Cmd.get('EI'); this.shift(); - this.imageCache.stream.reset(); - return this.imageCache.stream; - } - cacheImage = true; - } - if (!cacheImage && !this.imageCache.stream) { - this.imageCache.length = length; - this.imageCache.stream = null; + this.imageCache[adler32].reset(); + return this.imageCache[adler32]; + } } if (cipherTransform) { imageStream = cipherTransform.createStream(imageStream, length); } imageStream = this.filter(imageStream, dict, length); imageStream.dict = dict; - if (cacheImage) { + if (adler32 !== undefined) { imageStream.cacheKey = 'inline_' + length + '_' + adler32; - this.imageCache.adler32 = adler32; - this.imageCache.stream = imageStream; + this.imageCache[adler32] = imageStream; } this.buf2 = Cmd.get('EI'); this.shift(); return imageStream; }, fetchIfRef: function Parser_fetchIfRef(obj) { @@ -29750,32 +29871,16 @@ var Parser = (function ParserClosure() { } return new PredictorStream( new LZWStream(stream, maybeLength, earlyChange), maybeLength, params); } return new LZWStream(stream, maybeLength, earlyChange); } if (name === 'DCTDecode' || name === 'DCT') { - // According to the specification: for inline images, the ID operator - // shall be followed by a single whitespace character (unless it uses - // ASCII85Decode or ASCIIHexDecode filters). - // In practice this only seems to be followed for inline JPEG images, - // and generally ignoring the first byte of the stream if it is a - // whitespace char can even *cause* issues (e.g. in the CCITTFaxDecode - // filters used in issue2984.pdf). - // Hence when the first byte of the stream of an inline JPEG image is - // a whitespace character, we thus simply skip over it. - if (isCmd(this.buf1, 'ID')) { - var firstByte = stream.peekByte(); - if (firstByte === 0x0A /* LF */ || firstByte === 0x0D /* CR */ || - firstByte === 0x20 /* SPACE */) { - stream.skip(); - } - } xrefStreamStats[StreamType.DCT] = true; return new JpegStream(stream, maybeLength, stream.dict, this.xref); } if (name === 'JPXDecode' || name === 'JPX') { xrefStreamStats[StreamType.JPX] = true; return new JpxStream(stream, maybeLength, stream.dict); } if (name === 'ASCII85Decode' || name === 'A85') { @@ -31313,18 +31418,25 @@ var PredictorStream = (function Predicto * Depending on the type of JPEG a JpegStream is handled in different ways. For * JPEG's that are supported natively such as DeviceGray and DeviceRGB the image * data is stored and then loaded by the browser. For unsupported JPEG's we use * a library to decode these images and the stream behaves like all the other * DecodeStreams. */ var JpegStream = (function JpegStreamClosure() { function JpegStream(stream, maybeLength, dict, xref) { - // TODO: per poppler, some images may have 'junk' before that - // need to be removed + // Some images may contain 'junk' before the SOI (start-of-image) marker. + // Note: this seems to mainly affect inline images. + var ch; + while ((ch = stream.getByte()) !== -1) { + if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8). + stream.skip(-1); // Reset the stream position to the SOI. + break; + } + } this.stream = stream; this.maybeLength = maybeLength; this.dict = dict; DecodeStream.call(this, maybeLength); } JpegStream.prototype = Object.create(DecodeStream.prototype); @@ -32489,17 +32601,17 @@ var CCITTFaxStream = (function CCITTFaxS blackPixels ^= 1; } } var gotEOL = false; if (!this.eoblock && this.row === this.rows - 1) { this.eof = true; - } else if (this.eoline || !this.byteAlign) { + } else { code1 = this.lookBits(12); if (this.eoline) { while (code1 !== EOF && code1 !== 1) { this.eatBits(1); code1 = this.lookBits(12); } } else { while (code1 === 0) { @@ -34859,22 +34971,16 @@ var JpxImage = (function JpxImageClosure precinctsSizes.push({ PPx: precinctsSize & 0xF, PPy: precinctsSize >> 4 }); } cod.precinctsSizes = precinctsSizes; } var unsupported = []; - if (cod.sopMarkerUsed) { - unsupported.push('sopMarkerUsed'); - } - if (cod.ephMarkerUsed) { - unsupported.push('ephMarkerUsed'); - } if (cod.selectiveArithmeticCodingBypass) { unsupported.push('selectiveArithmeticCodingBypass'); } if (cod.resetContextProbabilities) { unsupported.push('resetContextProbabilities'); } if (cod.terminationOnEachCodingPass) { unsupported.push('terminationOnEachCodingPass'); @@ -35232,16 +35338,240 @@ var JpxImage = (function JpxImageClosure } i = 0; } l = 0; } throw new Error('JPX Error: Out of packets'); }; } + function ResolutionPositionComponentLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var l, r, c, p; + var maxDecompositionLevelsCount = 0; + for (c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + component.codingStyleParameters.decompositionLevelsCount); + } + var maxNumPrecinctsInLevel = new Int32Array( + maxDecompositionLevelsCount + 1); + for (r = 0; r <= maxDecompositionLevelsCount; ++r) { + var maxNumPrecincts = 0; + for (c = 0; c < componentsCount; ++c) { + var resolutions = tile.components[c].resolutions; + if (r < resolutions.length) { + maxNumPrecincts = Math.max(maxNumPrecincts, + resolutions[r].precinctParameters.numprecincts); + } + } + maxNumPrecinctsInLevel[r] = maxNumPrecincts; + } + l = 0; + r = 0; + c = 0; + p = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.3 Resolution-position-component-layer + for (; r <= maxDecompositionLevelsCount; r++) { + for (; p < maxNumPrecinctsInLevel[r]; p++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + if (p >= numprecincts) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, p, l); + l++; + return packet; + } + l = 0; + } + c = 0; + } + p = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function PositionComponentResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var precinctsIterationSizes = precinctsSizes; + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.4 Position-component-resolution-layer + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = + precinctsSizes.components[c].resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + c = 0; + } + px = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ComponentPositionResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.5 Component-position-resolution-layer + for (; c < componentsCount; ++c) { + var component = tile.components[c]; + var precinctsIterationSizes = precinctsSizes.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = precinctsIterationSizes.resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + px = 0; + } + py = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function getPrecinctIndexIfExist( + pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) { + var posX = pxIndex * precinctIterationSizes.minWidth; + var posY = pyIndex * precinctIterationSizes.minHeight; + if (posX % sizeInImageScale.width !== 0 || + posY % sizeInImageScale.height !== 0) { + return null; + } + var startPrecinctRowIndex = + (posY / sizeInImageScale.width) * + resolution.precinctParameters.numprecinctswide; + return (posX / sizeInImageScale.height) + startPrecinctRowIndex; + } + function getPrecinctSizesInImageScale(tile) { + var componentsCount = tile.components.length; + var minWidth = Number.MAX_VALUE; + var minHeight = Number.MAX_VALUE; + var maxNumWide = 0; + var maxNumHigh = 0; + var sizePerComponent = new Array(componentsCount); + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + var sizePerResolution = new Array(decompositionLevelsCount + 1); + var minWidthCurrentComponent = Number.MAX_VALUE; + var minHeightCurrentComponent = Number.MAX_VALUE; + var maxNumWideCurrentComponent = 0; + var maxNumHighCurrentComponent = 0; + var scale = 1; + for (var r = decompositionLevelsCount; r >= 0; --r) { + var resolution = component.resolutions[r]; + var widthCurrentResolution = + scale * resolution.precinctParameters.precinctWidth; + var heightCurrentResolution = + scale * resolution.precinctParameters.precinctHeight; + minWidthCurrentComponent = Math.min( + minWidthCurrentComponent, + widthCurrentResolution); + minHeightCurrentComponent = Math.min( + minHeightCurrentComponent, + heightCurrentResolution); + maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent, + resolution.precinctParameters.numprecinctswide); + maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent, + resolution.precinctParameters.numprecinctshigh); + sizePerResolution[r] = { + width: widthCurrentResolution, + height: heightCurrentResolution + }; + scale <<= 1; + } + minWidth = Math.min(minWidth, minWidthCurrentComponent); + minHeight = Math.min(minHeight, minHeightCurrentComponent); + maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent); + maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent); + sizePerComponent[c] = { + resolutions: sizePerResolution, + minWidth: minWidthCurrentComponent, + minHeight: minHeightCurrentComponent, + maxNumWide: maxNumWideCurrentComponent, + maxNumHigh: maxNumHighCurrentComponent + }; + } + return { + components: sizePerComponent, + minWidth: minWidth, + minHeight: minHeight, + maxNumWide: maxNumWide, + maxNumHigh: maxNumHigh + }; + } function buildPackets(context) { var siz = context.SIZ; var tileIndex = context.currentTile.index; var tile = context.tiles[tileIndex]; var componentsCount = siz.Csiz; // Creating resolutions and sub-bands for each component for (var c = 0; c < componentsCount; c++) { var component = tile.components[c]; @@ -35324,16 +35654,28 @@ var JpxImage = (function JpxImageClosure case 0: tile.packetsIterator = new LayerResolutionComponentPositionIterator(context); break; case 1: tile.packetsIterator = new ResolutionLayerComponentPositionIterator(context); break; + case 2: + tile.packetsIterator = + new ResolutionPositionComponentLayerIterator(context); + break; + case 3: + tile.packetsIterator = + new PositionComponentResolutionLayerIterator(context); + break; + case 4: + tile.packetsIterator = + new ComponentPositionResolutionLayerIterator(context); + break; default: throw new Error('JPX Error: Unsupported progression order ' + progressionOrder); } } function parseTilePackets(context, data, offset, dataLength) { var position = 0; var buffer, bufferSize = 0, skipNextBit = false; @@ -35351,16 +35693,31 @@ var JpxImage = (function JpxImageClosure } if (b === 0xFF) { skipNextBit = true; } } bufferSize -= count; return (buffer >>> bufferSize) & ((1 << count) - 1); } + function skipMarkerIfEqual(value) { + if (data[offset + position - 1] === 0xFF && + data[offset + position] === value) { + skipBytes(1); + return true; + } else if (data[offset + position] === 0xFF && + data[offset + position + 1] === value) { + skipBytes(2); + return true; + } + return false; + } + function skipBytes(count) { + position += count; + } function alignToByte() { bufferSize = 0; if (skipNextBit) { position++; skipNextBit = false; } } function readCodingpasses() { @@ -35378,23 +35735,29 @@ var JpxImage = (function JpxImageClosure if (value < 31) { return value + 6; } value = readBits(7); return value + 37; } var tileIndex = context.currentTile.index; var tile = context.tiles[tileIndex]; + var sopMarkerUsed = context.COD.sopMarkerUsed; + var ephMarkerUsed = context.COD.ephMarkerUsed; var packetsIterator = tile.packetsIterator; while (position < dataLength) { alignToByte(); + if (sopMarkerUsed && skipMarkerIfEqual(0x91)) { + // Skip also marker segment length and packet sequence ID + skipBytes(4); + } + var packet = packetsIterator.nextPacket(); if (!readBits(1)) { continue; } - var packet = packetsIterator.nextPacket(); var layerNumber = packet.layerNumber; var queue = [], codeblock; for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) { codeblock = packet.codeblocks[i]; var precinct = codeblock.precinct; var codeblockColumn = codeblock.cbx - precinct.cbxMin; var codeblockRow = codeblock.cby - precinct.cbyMin; var codeblockIncluded = false; @@ -35463,16 +35826,19 @@ var JpxImage = (function JpxImageClosure var codedDataLength = readBits(bits); queue.push({ codeblock: codeblock, codingpasses: codingpasses, dataLength: codedDataLength }); } alignToByte(); + if (ephMarkerUsed) { + skipMarkerIfEqual(0x92); + } while (queue.length > 0) { var packetItem = queue.shift(); codeblock = packetItem.codeblock; if (codeblock['data'] === undefined) { codeblock.data = []; } codeblock.data.push({ data: data,
--- a/browser/extensions/pdfjs/content/web/viewer.css +++ b/browser/extensions/pdfjs/content/web/viewer.css @@ -98,21 +98,16 @@ border: 0; } :fullscreen .pdfViewer .page { margin-bottom: 100%; border: 0; } -.pdfViewer .page .annotationHighlight { - position: absolute; - border: 2px #FFFF99 solid; -} - .pdfViewer .page .annotText > img { position: absolute; cursor: pointer; } .pdfViewer .page .annotTextContentWrapper { position: absolute; width: 20em; @@ -806,17 +801,16 @@ html[dir='rtl'] .dropdownToolbarButton { html[dir='ltr'] .dropdownToolbarButton { background-position: 95%; } html[dir='rtl'] .dropdownToolbarButton { background-position: 5%; } .dropdownToolbarButton > select { - -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */ min-width: 140px; font-size: 12px; color: hsl(0,0%,95%); margin: 0; padding: 0; border: none; background: rgba(0,0,0,0); /* Opera does not support 'transparent' <select> background */ } @@ -1749,16 +1743,20 @@ html[dir='rtl'] #documentPropertiesOverl display: block; } #printContainer canvas { position: relative; top: 0; left: 0; display: block; } + #printContainer div { + page-break-after: always; + page-break-inside: avoid; + } } .visibleLargeView, .visibleMediumView, .visibleSmallView { display: none; }
new file mode 100644 --- /dev/null +++ b/docshell/base/TimelineMarker.cpp @@ -0,0 +1,42 @@ +/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=2 sw=2 tw=80 et: + * + * 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/. */ + +#include "nsDocShell.h" +#include "TimelineMarker.h" + +TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName, + TracingMetadata aMetaData) + : mName(aName) + , mMetaData(aMetaData) +{ + MOZ_COUNT_CTOR(TimelineMarker); + MOZ_ASSERT(aName); + aDocShell->Now(&mTime); + if (aMetaData == TRACING_INTERVAL_START) { + CaptureStack(); + } +} + +TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName, + TracingMetadata aMetaData, + const nsAString& aCause) + : mName(aName) + , mMetaData(aMetaData) + , mCause(aCause) +{ + MOZ_COUNT_CTOR(TimelineMarker); + MOZ_ASSERT(aName); + aDocShell->Now(&mTime); + if (aMetaData == TRACING_INTERVAL_START) { + CaptureStack(); + } +} + +TimelineMarker::~TimelineMarker() +{ + MOZ_COUNT_DTOR(TimelineMarker); +}
new file mode 100644 --- /dev/null +++ b/docshell/base/TimelineMarker.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=2 sw=2 tw=80 et: + * + * 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/. */ + +#ifndef TimelineMarker_h__ +#define TimelineMarker_h__ + +#include "nsString.h" +#include "GeckoProfiler.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "nsContentUtils.h" +#include "jsapi.h" + +class nsDocShell; + +// Objects of this type can be added to the timeline. The class can +// also be subclassed to let a given marker creator provide custom +// details. +class TimelineMarker +{ +public: + TimelineMarker(nsDocShell* aDocShell, const char* aName, + TracingMetadata aMetaData); + + TimelineMarker(nsDocShell* aDocShell, const char* aName, + TracingMetadata aMetaData, + const nsAString& aCause); + + virtual ~TimelineMarker(); + + // Check whether two markers should be considered the same, + // for the purpose of pairing start and end markers. Normally + // this definition suffices. + virtual bool Equals(const TimelineMarker* other) + { + return strcmp(mName, other->mName) == 0; + } + + // Add details specific to this marker type to aMarker. The + // standard elements have already been set. This method is + // called on both the starting and ending markers of a pair. + // Ordinarily the ending marker doesn't need to do anything + // here. + virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) + { + } + + virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&) + { + MOZ_ASSERT_UNREACHABLE("can only be called on layer markers"); + } + + const char* GetName() const + { + return mName; + } + + TracingMetadata GetMetaData() const + { + return mMetaData; + } + + DOMHighResTimeStamp GetTime() const + { + return mTime; + } + + const nsString& GetCause() const + { + return mCause; + } + + JSObject* GetStack() + { + if (mStackTrace) { + return mStackTrace->get(); + } + return nullptr; + } + +protected: + + void CaptureStack() + { + JSContext* ctx = nsContentUtils::GetCurrentJSContext(); + if (ctx) { + JS::RootedObject stack(ctx); + if (JS::CaptureCurrentStack(ctx, &stack)) { + mStackTrace.emplace(ctx, stack.get()); + } else { + JS_ClearPendingException(ctx); + } + } + } + +private: + + const char* mName; + TracingMetadata mMetaData; + DOMHighResTimeStamp mTime; + nsString mCause; + + // While normally it is not a good idea to make a persistent + // root, in this case changing nsDocShell to participate in + // cycle collection was deemed too invasive, the stack trace + // can't actually cause a cycle, and the markers are only held + // here temporarily to boot. + mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace; +}; + +#endif /* TimelineMarker_h__ */
--- a/docshell/base/moz.build +++ b/docshell/base/moz.build @@ -57,16 +57,17 @@ UNIFIED_SOURCES += [ 'nsDocShellEditorData.cpp', 'nsDocShellEnumerator.cpp', 'nsDocShellLoadInfo.cpp', 'nsDocShellTransferableHooks.cpp', 'nsDownloadHistory.cpp', 'nsDSURIContentListener.cpp', 'nsWebNavigationInfo.cpp', 'SerializedLoadContext.cpp', + 'TimelineMarker.cpp', ] FAIL_ON_WARNINGS = True MSVC_ENABLE_PGO = True include('/ipc/chromium/chromium-config.mozbuild')
--- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -27,16 +27,17 @@ // Helper Classes #include "nsCOMPtr.h" #include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences #include "nsString.h" #include "nsAutoPtr.h" #include "nsThreadUtils.h" #include "nsContentUtils.h" +#include "TimelineMarker.h" // Threshold value in ms for META refresh based redirects #define REFRESH_REDIRECT_TIMER 15000 // Interfaces Needed #include "nsIDocCharset.h" #include "nsIInterfaceRequestor.h" #include "nsIRefreshURI.h" @@ -255,135 +256,16 @@ public: // Notify Scroll observers when an async panning/zooming transform // has started being applied void NotifyAsyncPanZoomStarted(const mozilla::CSSIntPoint aScrollPos); // Notify Scroll observers when an async panning/zooming transform // is no longer applied void NotifyAsyncPanZoomStopped(const mozilla::CSSIntPoint aScrollPos); - // Objects of this type can be added to the timeline. The class - // can also be subclassed to let a given marker creator provide - // custom details. - class TimelineMarker - { - public: - TimelineMarker(nsDocShell* aDocShell, const char* aName, - TracingMetadata aMetaData) - : mName(aName) - , mMetaData(aMetaData) - { - MOZ_COUNT_CTOR(TimelineMarker); - MOZ_ASSERT(aName); - aDocShell->Now(&mTime); - if (aMetaData == TRACING_INTERVAL_START) { - CaptureStack(); - } - } - - TimelineMarker(nsDocShell* aDocShell, const char* aName, - TracingMetadata aMetaData, - const nsAString& aCause) - : mName(aName) - , mMetaData(aMetaData) - , mCause(aCause) - { - MOZ_COUNT_CTOR(TimelineMarker); - MOZ_ASSERT(aName); - aDocShell->Now(&mTime); - if (aMetaData == TRACING_INTERVAL_START) { - CaptureStack(); - } - } - - virtual ~TimelineMarker() - { - MOZ_COUNT_DTOR(TimelineMarker); - } - - // Check whether two markers should be considered the same, - // for the purpose of pairing start and end markers. Normally - // this definition suffices. - virtual bool Equals(const TimelineMarker* other) - { - return strcmp(mName, other->mName) == 0; - } - - // Add details specific to this marker type to aMarker. The - // standard elements have already been set. This method is - // called on both the starting and ending markers of a pair. - // Ordinarily the ending marker doesn't need to do anything - // here. - virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) - { - } - - virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&) - { - MOZ_ASSERT_UNREACHABLE("can only be called on layer markers"); - } - - const char* GetName() const - { - return mName; - } - - TracingMetadata GetMetaData() const - { - return mMetaData; - } - - DOMHighResTimeStamp GetTime() const - { - return mTime; - } - - const nsString& GetCause() const - { - return mCause; - } - - JSObject* GetStack() - { - if (mStackTrace) { - return mStackTrace->get(); - } - return nullptr; - } - - protected: - - void CaptureStack() - { - JSContext* ctx = nsContentUtils::GetCurrentJSContext(); - if (ctx) { - JS::RootedObject stack(ctx); - if (JS::CaptureCurrentStack(ctx, &stack)) { - mStackTrace.emplace(ctx, stack.get()); - } else { - JS_ClearPendingException(ctx); - } - } - } - - private: - - const char* mName; - TracingMetadata mMetaData; - DOMHighResTimeStamp mTime; - nsString mCause; - - // While normally it is not a good idea to make a persistent - // root, in this case changing nsDocShell to participate in - // cycle collection was deemed too invasive, the stack trace - // can't actually cause a cycle, and the markers are only held - // here temporarily to boot. - mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace; - }; - // Add new profile timeline markers to this docShell. This will only add // markers if the docShell is currently recording profile timeline markers. // See nsIDocShell::recordProfileTimelineMarkers void AddProfileTimelineMarker(const char* aName, TracingMetadata aMetaData); void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker> &aMarker); // Global counter for how many docShells are currently recording profile
--- a/dom/base/Console.cpp +++ b/dom/base/Console.cpp @@ -800,32 +800,32 @@ ReifyStack(nsIStackFrame* aStack, nsTArr NS_ENSURE_SUCCESS(rv, rv); stack.swap(caller); } return NS_OK; } -class ConsoleTimelineMarker : public nsDocShell::TimelineMarker +class ConsoleTimelineMarker : public TimelineMarker { public: ConsoleTimelineMarker(nsDocShell* aDocShell, TracingMetadata aMetaData, const nsAString& aCause) - : nsDocShell::TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause) + : TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause) { if (aMetaData == TRACING_INTERVAL_END) { CaptureStack(); } } - virtual bool Equals(const nsDocShell::TimelineMarker* aOther) + virtual bool Equals(const TimelineMarker* aOther) { - if (!nsDocShell::TimelineMarker::Equals(aOther)) { + if (!TimelineMarker::Equals(aOther)) { return false; } // Console markers must have matching causes as well. return GetCause() == aOther->GetCause(); } virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { @@ -964,17 +964,17 @@ Console::Method(JSContext* aCx, MethodNa } if (isTimelineRecording && aData.Length() == 1) { JS::Rooted<JS::Value> value(aCx, aData[0]); JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); if (jsString) { nsAutoJSString key; if (key.init(aCx, jsString)) { - mozilla::UniquePtr<nsDocShell::TimelineMarker> marker = + mozilla::UniquePtr<TimelineMarker> marker = MakeUnique<ConsoleTimelineMarker>(docShell, aMethodName == MethodTime ? TRACING_INTERVAL_START : TRACING_INTERVAL_END, key); docShell->AddProfileTimelineMarker(marker); } } }
--- a/dom/events/EventListenerManager.cpp +++ b/dom/events/EventListenerManager.cpp @@ -1019,22 +1019,22 @@ EventListenerManager::GetDocShellForTarg if (doc) { docShell = doc->GetDocShell(); } return docShell; } -class EventTimelineMarker : public nsDocShell::TimelineMarker +class EventTimelineMarker : public TimelineMarker { public: EventTimelineMarker(nsDocShell* aDocShell, TracingMetadata aMetaData, uint16_t aPhase, const nsAString& aCause) - : nsDocShell::TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause) + : TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause) , mPhase(aPhase) { } virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { if (GetMetaData() == TRACING_INTERVAL_START) { aMarker.mType.Construct(GetCause()); @@ -1109,17 +1109,17 @@ EventListenerManager::HandleEventInterna docShell->GetRecordProfileTimelineMarkers(&isTimelineRecording); } if (isTimelineRecording) { nsDocShell* ds = static_cast<nsDocShell*>(docShell.get()); nsAutoString typeStr; (*aDOMEvent)->GetType(typeStr); uint16_t phase; (*aDOMEvent)->GetEventPhase(&phase); - mozilla::UniquePtr<nsDocShell::TimelineMarker> marker = + mozilla::UniquePtr<TimelineMarker> marker = MakeUnique<EventTimelineMarker>(ds, TRACING_INTERVAL_START, phase, typeStr); ds->AddProfileTimelineMarker(marker); } } if (NS_FAILED(HandleEventSubType(listener, *aDOMEvent, aCurrentTarget))) {
--- a/layout/base/FrameLayerBuilder.cpp +++ b/layout/base/FrameLayerBuilder.cpp @@ -4466,21 +4466,21 @@ static void DrawForcedBackgroundColor(Dr { if (NS_GET_A(aBackgroundColor) > 0) { nsIntRect r = aLayer->GetVisibleRegion().GetBounds(); ColorPattern color(ToDeviceColor(aBackgroundColor)); aDrawTarget.FillRect(Rect(r.x, r.y, r.width, r.height), color); } } -class LayerTimelineMarker : public nsDocShell::TimelineMarker +class LayerTimelineMarker : public TimelineMarker { public: LayerTimelineMarker(nsDocShell* aDocShell, const nsIntRegion& aRegion) - : nsDocShell::TimelineMarker(aDocShell, "Layer", TRACING_EVENT) + : TimelineMarker(aDocShell, "Layer", TRACING_EVENT) , mRegion(aRegion) { } ~LayerTimelineMarker() { } @@ -4648,17 +4648,17 @@ FrameLayerBuilder::DrawPaintedLayer(Pain FlashPaint(aContext); } if (presContext && presContext->GetDocShell() && isActiveLayerManager) { nsDocShell* docShell = static_cast<nsDocShell*>(presContext->GetDocShell()); bool isRecording; docShell->GetRecordProfileTimelineMarkers(&isRecording); if (isRecording) { - mozilla::UniquePtr<nsDocShell::TimelineMarker> marker = + mozilla::UniquePtr<TimelineMarker> marker = MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw); docShell->AddProfileTimelineMarker(marker); } } if (!aRegionToInvalidate.IsEmpty()) { aLayer->AddInvalidRect(aRegionToInvalidate.GetBounds()); }
--- a/mobile/android/base/menu/GeckoMenuItem.java +++ b/mobile/android/base/menu/GeckoMenuItem.java @@ -439,17 +439,19 @@ public class GeckoMenuItem implements Me @Override public MenuItem setTitleCondensed(CharSequence title) { mTitleCondensed = title; return this; } @Override public MenuItem setVisible(boolean visible) { - if (mVisible != visible) { + // Action views are not normal menu items and visibility can get out + // of sync unless we dispatch whenever required. + if (isActionItem() || mVisible != visible) { mVisible = visible; if (mShouldDispatchChanges) { mMenu.onItemChanged(this); } else { mDidChange = true; } } return this;
--- a/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml +++ b/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml @@ -1,17 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> <!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:gecko="http://schemas.android.com/apk/res-auto" style="@style/TabsItem" - android:focusable="true" android:id="@+id/info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" @@ -32,17 +31,17 @@ android:singleLine="true" android:duplicateParentState="true" gecko:fadeWidth="15dp" android:paddingRight="5dp"/> <!-- Use of baselineAlignBottom only supported from API 11+ - if this needs to work on lower API versions we'll need to override getBaseLine() and return image height, but we assume this won't happen --> - <ImageButton android:id="@+id/close" + <ImageView android:id="@+id/close" style="@style/TabsItemClose" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="center" android:baselineAlignBottom="true" android:background="@android:color/transparent" android:contentDescription="@string/close_tab" android:src="@drawable/new_tablet_tab_item_close_button"
--- a/mobile/android/base/tabs/TabsGridLayout.java +++ b/mobile/android/base/tabs/TabsGridLayout.java @@ -22,16 +22,17 @@ import android.content.res.TypedArray; import android.graphics.PointF; import android.util.AttributeSet; import android.util.SparseArray; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.DecelerateInterpolator; +import android.widget.AdapterView; import android.widget.Button; import android.widget.GridView; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.PropertyValuesHolder; import com.nineoldandroids.animation.ValueAnimator; @@ -89,48 +90,48 @@ class TabsGridLayout extends GridView final Resources resources = getResources(); mColumnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width); setColumnWidth(mColumnWidth); final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding); final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top); setPadding(padding, paddingTop, padding, padding); + + setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + TabsLayoutItemView tab = (TabsLayoutItemView) view; + Tabs.getInstance().selectTab(tab.getTabId()); + autoHidePanel(); + } + }); } private class TabsGridLayoutAdapter extends TabsLayoutAdapter { final private Button.OnClickListener mCloseClickListener; - final private View.OnClickListener mSelectClickListener; public TabsGridLayoutAdapter (Context context) { super(context, R.layout.new_tablet_tabs_item_cell); mCloseClickListener = new Button.OnClickListener() { @Override public void onClick(View v) { closeTab(v); } }; - - mSelectClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - TabsLayoutItemView tab = (TabsLayoutItemView) v; - Tabs.getInstance().selectTab(tab.getTabId()); - autoHidePanel(); - } - }; } @Override TabsLayoutItemView newView(int position, ViewGroup parent) { final TabsLayoutItemView item = super.newView(position, parent); - item.setOnClickListener(mSelectClickListener); + item.setCloseOnClickListener(mCloseClickListener); + return item; } @Override public void bindView(TabsLayoutItemView view, Tab tab) { super.bindView(view, tab); // If we're recycling this view, there's a chance it was transformed during
--- a/mobile/android/base/tabs/TabsLayoutAdapter.java +++ b/mobile/android/base/tabs/TabsLayoutAdapter.java @@ -41,16 +41,17 @@ public class TabsLayoutAdapter extends B if (tabRemoved) { notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. } return tabRemoved; } final void clear() { mTabs = null; + notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. } @Override public int getCount() { return (mTabs == null ? 0 : mTabs.size()); } @@ -67,16 +68,21 @@ public class TabsLayoutAdapter extends B final int getPositionForTab(Tab tab) { if (mTabs == null || tab == null) return -1; return mTabs.indexOf(tab); } @Override + public boolean isEnabled(int position) { + return true; + } + + @Override final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) { final TabsLayoutItemView view; if (convertView == null) { view = newView(position, parent); } else { view = (TabsLayoutItemView) convertView; } final Tab tab = mTabs.get(position);
--- a/mobile/android/base/tabs/TabsLayoutItemView.java +++ b/mobile/android/base/tabs/TabsLayoutItemView.java @@ -27,17 +27,17 @@ public class TabsLayoutItemView extends implements Checkable { private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName(); private static final int[] STATE_CHECKED = { android.R.attr.state_checked }; private boolean mChecked; private int mTabId; private TextView mTitle; private ImageView mThumbnail; - private ImageButton mCloseButton; + private ImageView mCloseButton; private TabThumbnailWrapper mThumbnailWrapper; public TabsLayoutItemView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public int[] onCreateDrawableState(int extraSpace) { @@ -46,16 +46,21 @@ public class TabsLayoutItemView extends if (mChecked) { mergeDrawableStates(drawableState, STATE_CHECKED); } return drawableState; } @Override + public boolean isEnabled() { + return true; + } + + @Override public boolean isChecked() { return mChecked; } @Override public void setChecked(boolean checked) { if (mChecked == checked) { return; @@ -82,17 +87,17 @@ public class TabsLayoutItemView extends mCloseButton.setOnClickListener(mOnClickListener); } @Override protected void onFinishInflate() { super.onFinishInflate(); mTitle = (TextView) findViewById(R.id.title); mThumbnail = (ImageView) findViewById(R.id.thumbnail); - mCloseButton = (ImageButton) findViewById(R.id.close); + mCloseButton = (ImageView) findViewById(R.id.close); mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper); if (NewTabletUI.isEnabled(getContext())) { growCloseButtonHitArea(); } } private void growCloseButtonHitArea() {
--- a/toolkit/content/widgets/preferences.xml +++ b/toolkit/content/widgets/preferences.xml @@ -127,18 +127,22 @@ if (!this.name) return; this.preferences.rootBranchInternal .addObserver(this.name, this.preferences, false); // In non-instant apply mode, we must try and use the last saved state // from any previous opens of a child dialog instead of the value from // preferences, to pick up any edits a user may have made. + + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); if (this.preferences.type == "child" && - !this.instantApply && window.opener) { + !this.instantApply && window.opener && + secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) { var pdoc = window.opener.document; // Try to find a preference element for the same preference. var preference = null; var parentPreferences = pdoc.getElementsByTagName("preferences"); for (var k = 0; (k < parentPreferences.length && !preference); ++k) { var parentPrefs = parentPreferences[k] .getElementsByAttribute("name", this.name); @@ -1048,17 +1052,20 @@ </implementation> <handlers> <handler event="dialogaccept"> <![CDATA[ if (!this._fireEvent("beforeaccept", this)){ return false; } - if (this.type == "child" && window.opener) { + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); + if (this.type == "child" && window.opener && + secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) { var psvc = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefBranch); var instantApply = psvc.getBoolPref("browser.preferences.instantApply"); if (instantApply) { var panes = this.preferencePanes; for (var i = 0; i < panes.length; ++i) panes[i].writePreferences(true); }
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/actors/director-manager.js @@ -0,0 +1,780 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 events = require("sdk/event/core"); +const protocol = require("devtools/server/protocol"); + +const { Cu, Ci } = require("chrome"); + +const { on, once, off, emit } = events; +const { method, Arg, Option, RetVal, types } = protocol; + +const { sandbox, evaluate } = require('sdk/loader/sandbox'); +const { Class } = require("sdk/core/heritage"); + +const { PlainTextConsole } = require('sdk/console/plain-text'); + +const { DirectorRegistry } = require("./director-registry"); + +/** + * E10S child setup helper + */ + +const {DebuggerServer} = require("devtools/server/main"); + +/** + * Error Messages + */ + +const ERR_MESSAGEPORT_FINALIZED = "message port finalized"; + +const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id"; +const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id"; + +/** + * Type describing a messageport event + */ +types.addDictType("messageportevent", { + isTrusted: "boolean", + data: "nullable:primitive", + origin: "nullable:string", + lastEventId: "nullable:string", + source: "messageport", + ports: "nullable:array:messageport" +}); + +/** + * A MessagePort Actor allowing communication through messageport events + * over the remote debugging protocol. + */ +let MessagePortActor = exports.MessagePortActor = protocol.ActorClass({ + typeName: "messageport", + + /** + * Create a MessagePort actor. + * + * @param DebuggerServerConnection conn + * The server connection. + * @param MessagePort port + * The wrapped MessagePort. + */ + initialize: function(conn, port) { + protocol.Actor.prototype.initialize.call(this, conn); + + // NOTE: can't get a weak reference because we need to subscribe events + // using port.onmessage or addEventListener + this.port = port; + }, + + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this.finalize(); + }, + + /** + * Sends a message on the wrapped message port. + * + * @param Object msg + * The JSON serializable message event payload + */ + postMessage: method(function (msg) { + if (!this.port) { + console.error(ERR_MESSAGEPORT_FINALIZED); + return; + } + + this.port.postMessage(msg); + }, { + oneway: true, + request: { + msg: Arg(0, "nullable:json") + } + }), + + /** + * Starts to receive and send queued messages on this message port. + */ + start: method(function () { + if (!this.port) { + console.error(ERR_MESSAGEPORT_FINALIZED); + return; + } + + // NOTE: set port.onmessage to a function is an implicit start + // and starts to send queued messages. + // On the client side we should set MessagePortClient.onmessage + // to a setter which register an handler to the message event + // and call the actor start method to start receiving messages + // from the MessagePort's queue. + this.port.onmessage = (evt) => { + var ports; + + // TODO: test these wrapped ports + if (Array.isArray(evt.ports)) { + ports = evt.ports.map((port) => { + let actor = new MessagePortActor(this.conn, port); + this.manage(actor); + return actor; + }); + } + + emit(this, "message", { + isTrusted: evt.isTrusted, + data: evt.data, + origin: evt.origin, + lastEventId: evt.lastEventId, + source: this, + ports: ports + }); + }; + }, { + oneway: true, + request: {} + }), + + /** + * Starts to receive and send queued messages on this message port, or + * raise an exception if the port is null + */ + close: method(function () { + if (!this.port) { + console.error(ERR_MESSAGEPORT_FINALIZED); + return; + } + + this.port.onmessage = null; + this.port.close(); + }, { + oneway: true, + request: {} + }), + + finalize: method(function () { + this.close(); + this.port = null; + }, { + oneway: true + }), + + /** + * Events emitted by this actor. + */ + events: { + "message": { + type: "message", + msg: Arg(0, "nullable:messageportevent") + } + } +}); + +/** + * The corresponding Front object for the MessagePortActor. + */ +let MessagePortFront = exports.MessagePortFront = protocol.FrontClass(MessagePortActor, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + + +/** + * Type describing a director-script error + */ +types.addDictType("director-script-error", { + directorScriptId: "string", + message: "string", + stack: "string", + fileName: "string", + lineNumber: "number", + columnNumber: "number" +}); + +/** + * Type describing a director-script attach event + */ +types.addDictType("director-script-attach", { + directorScriptId: "string", + url: "string", + innerId: "number", + port: "nullable:messageport" +}); + +/** + * Type describing a director-script detach event + */ +types.addDictType("director-script-detach", { + directorScriptId: "string", + innerId: "number" +}); + +/** + * The Director Script Actor manage javascript code running in a non-privileged sandbox with the same + * privileges of the target global (browser tab or a firefox os app). + * + * After retrieving an instance of this actor (from the tab director actor), you'll need to set it up + * by calling setup(). + * + * After the setup, this actor will automatically attach/detach the content script (and optionally a + * directly connect the debugger client and the content script using a MessageChannel) on tab + * navigation. + */ +let DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClass({ + typeName: "director-script", + + /** + * Events emitted by this actor. + */ + events: { + "error": { + type: "error", + data: Arg(0, "director-script-error") + }, + "attach": { + type: "attach", + data: Arg(0, "director-script-attach") + }, + "detach": { + type: "detach", + data: Arg(0, "director-script-detach") + } + }, + + /** + * Creates the director script actor + * + * @param DebuggerServerConnection conn + * The server connection. + * @param Actor tabActor + * The tab (or root) actor. + * @param String scriptId + * The director-script id. + * @param String scriptCode + * The director-script javascript source. + * @param Object scriptOptions + * The director-script options object. + */ + initialize: function(conn, tabActor, { scriptId, scriptCode, scriptOptions }) { + protocol.Actor.prototype.initialize.call(this, conn, tabActor); + + this.tabActor = tabActor; + + this._scriptId = scriptId; + this._scriptCode = scriptCode; + this._scriptOptions = scriptOptions; + this._setupCalled = false; + + this._onGlobalCreated = this._onGlobalCreated.bind(this); + this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this); + }, + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + + this.finalize(); + }, + + /** + * Starts listening to the tab global created, in order to create the director-script sandbox + * using the configured scriptCode, attached/detached automatically to the tab + * window on tab navigation. + * + * @param Boolean reload + * attach the page immediately or reload it first. + * @param Boolean skipAttach + * skip the attach + */ + setup: method(function ({ reload, skipAttach }) { + if (this._setupCalled) { + // do nothing + return; + } + + this._setupCalled = true; + + on(this.tabActor, "window-ready", this._onGlobalCreated); + on(this.tabActor, "window-destroyed", this._onGlobalDestroyed); + + // optional skip attach (needed by director-manager for director scripts bulk activation) + if (skipAttach) { + return; + } + + if (reload) { + this.window.location.reload(); + } else { + // fake a global created event to attach without reload + this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true }); + } + }, { + request: { + reload: Option(0, "boolean"), + skipAttach: Option(0, "boolean") + }, + oneway: true + }), + + /** + * Get the attached MessagePort actor if any + */ + getMessagePort: method(function () { + return this._messagePortActor; + }, { + request: { }, + response: { + port: RetVal("nullable:messageport") + } + }), + + /** + * Stop listening for document global changes, destroy the content worker and puts + * this actor to hibernation. + */ + finalize: method(function () { + if (!this._setupCalled) { + return; + } + + off(this.tabActor, "window-ready", this._onGlobalCreated); + off(this.tabActor, "window-destroyed", this._onGlobalDestroyed); + + this._onGlobalDestroyed({ id: this._lastAttachedWinId }); + + this._setupCalled = false; + }, { + oneway: true + }), + + // local helpers + get window() { + return this.tabActor.window; + }, + + /* event handlers */ + _onGlobalCreated: function({ id, window, isTopLevel }) { + if (!isTopLevel) { + // filter iframes + return; + } + + if (this._lastAttachedWinId) { + // if we have received a global created without a previous global destroyed, + // it's time to cleanup the previous state + this._onGlobalDestroyed(this._lastAttachedWinId); + } + + // TODO: check if we want to share a single sandbox per global + // for multiple debugger clients + + // create & attach the new sandbox + this._scriptSandbox = new DirectorScriptSandbox({ + scriptId: this._scriptId, + scriptCode: this._scriptCode, + scriptOptions: this._scriptOptions + }); + + try { + // attach the global window + this._lastAttachedWinId = id; + var port = this._scriptSandbox.attach(window, id); + this._onDirectorScriptAttach(window, port); + } catch(e) { + this._onDirectorScriptError(e); + } + }, + _onGlobalDestroyed: function({ id }) { + if (id !== this._lastAttachedWinId) { + // filter destroyed globals + return; + } + + // unmanage and cleanup the messageport actor + if (this._messagePortActor) { + this.unmanage(this._messagePortActor); + this._messagePortActor = null; + } + + // NOTE: destroy here the old worker + if (this._scriptSandbox) { + this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this)); + + // send a detach event to the debugger client + emit(this, "detach", { + directorScriptId: this._scriptId, + innerId: this._lastAttachedWinId + }); + + this._lastAttachedWinId = null; + this._scriptSandbox = null; + } + }, + _onDirectorScriptError: function(error) { + // route the content script error to the debugger client + emit(this, "error", { + directorScriptId: this._scriptId, + message: error.toString(), + stack: error.stack, + fileName: error.fileName, + lineNumber: error.lineNumber, + columnNumber: error.columnNumber + }); + }, + _onDirectorScriptAttach: function(window, port) { + let portActor = new MessagePortActor(this.conn, port); + this.manage(portActor); + this._messagePortActor = portActor; + + emit(this, "attach", { + directorScriptId: this._scriptId, + url: (window && window.location) ? window.location.toString() : "", + innerId: this._lastAttachedWinId, + port: this._messagePortActor + }); + } +}); + +/** + * The corresponding Front object for the DirectorScriptActor. + */ +let DirectorScriptFront = exports.DirectorScriptFront = protocol.FrontClass(DirectorScriptActor, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +/** + * The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts. + */ +const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClass({ + typeName: "director-manager", + + /** + * Events emitted by this actor. + */ + events: { + "director-script-error": { + type: "error", + data: Arg(0, "director-script-error") + }, + "director-script-attach": { + type: "attach", + data: Arg(0, "director-script-attach") + }, + "director-script-detach": { + type: "detach", + data: Arg(0, "director-script-detach") + } + }, + + /* init & destroy methods */ + initialize: function(conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + this._directorScriptActorsMap = new Map(); + }, + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this.finalize(); + }, + + /** + * Retrieves the list of installed director-scripts. + */ + list: method(function () { + var enabled_script_ids = [for (id of this._directorScriptActorsMap.keys()) id]; + + return { + installed: DirectorRegistry.list(), + enabled: enabled_script_ids + }; + }, { + response: { + directorScripts: RetVal("json") + } + }), + + /** + * Bulk enabling director-scripts. + * + * @param Array[String] selectedIds + * The list of director-script ids to be enabled, + * ["*"] will activate all the installed director-scripts + * @param Boolean reload + * optionally reload the target window + */ + enableByScriptIds: method(function(selectedIds, { reload }) { + if (selectedIds && selectedIds.length === 0) { + // filtered all director scripts ids + return; + } + + for (let scriptId of DirectorRegistry.list()) { + // filter director script ids + if (selectedIds.indexOf("*") < 0 && + selectedIds.indexOf(scriptId) < 0) { + continue; + } + + let actor = this.getByScriptId(scriptId); + + // skip attach if reload is true (activated director scripts + // will be automatically attached on the final reload) + actor.setup({ reload: false, skipAttach: reload }); + } + + if (reload) { + this.tabActor.window.location.reload(); + } + }, { + oneway: true, + request: { + selectedIds: Arg(0, "array:string"), + reload: Option(1, "boolean") + } + }), + + /** + * Bulk disabling director-scripts. + * + * @param Array[String] selectedIds + * The list of director-script ids to be disable, + * ["*"] will de-activate all the enable director-scripts + * @param Boolean reload + * optionally reload the target window + */ + disableByScriptIds: method(function(selectedIds, { reload }) { + if (selectedIds && selectedIds.length === 0) { + // filtered all director scripts ids + return; + } + + for (let scriptId of this._directorScriptActorsMap.keys()) { + // filter director script ids + if (selectedIds.indexOf("*") < 0 && + selectedIds.indexOf(scriptId) < 0) { + continue; + } + + let actor = this._directorScriptActorsMap.get(scriptId); + this._directorScriptActorsMap.delete(scriptId); + + // finalize the actor (which will produce director-script-detach event) + actor.finalize(); + // unsubscribe event handlers on the disabled actor + off(actor); + + this.unmanage(actor); + } + + if (reload) { + this.tabActor.window.location.reload(); + } + }, { + oneway: true, + request: { + selectedIds: Arg(0, "array:string"), + reload: Option(1, "boolean") + } + }), + + /** + * Retrieves the actor instance of an installed director-script + * (and create the actor instance if it doesn't exists yet). + */ + getByScriptId: method(function(scriptId) { + var id = scriptId; + // raise an unknown director-script id exception + if (!DirectorRegistry.checkInstalled(id)) { + console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id); + throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID); + } + + // get a previous created actor instance + let actor = this._directorScriptActorsMap.get(id); + + // create a new actor instance + if (!actor) { + let directorScriptDefinition = DirectorRegistry.get(id); + + // test lazy director-script (e.g. uninstalled in the parent process) + if (!directorScriptDefinition) { + + console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id); + throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID); + } + + actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition); + this._directorScriptActorsMap.set(id, actor); + + on(actor, "error", emit.bind(null, this, "director-script-error")); + on(actor, "attach", emit.bind(null, this, "director-script-attach")); + on(actor, "detach", emit.bind(null, this, "director-script-detach")); + + this.manage(actor); + } + + return actor; + }, { + request: { + scriptId: Arg(0, "string") + }, + response: { + directorScript: RetVal("director-script") + } + }), + + finalize: method(function() { + this.disableByScriptIds(["*"], false); + }, { + oneway: true + }) +}); + +/** + * The corresponding Front object for the DirectorManagerActor. + */ +exports.DirectorManagerFront = protocol.FrontClass(DirectorManagerActor, { + initialize: function(client, { directorManagerActor }) { + protocol.Front.prototype.initialize.call(this, client, { + actor: directorManagerActor + }); + this.manage(this); + } +}); + +/* private helpers */ + +/** + * DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox + * to a target window. + */ +const DirectorScriptSandbox = Class({ + initialize: function({scriptId, scriptCode, scriptOptions}) { + this._scriptId = scriptId; + this._scriptCode = scriptCode; + this._scriptOptions = scriptOptions; + }, + + attach: function(window, innerId) { + this._innerId = innerId, + this._window = window; + this._proto = Cu.createObjectIn(this._window); + + var id = this._scriptId; + var uri = this._scriptCode; + + this._sandbox = sandbox(window, { + sandboxName: uri, + sandboxPrototype: this._proto, + sameZoneAs: window, + wantXrays: true, + wantComponents: false, + wantExportHelpers: false, + metadata: { + URI: uri, + addonID: id, + SDKDirectorScript: true, + "inner-window-id": innerId + } + }); + + // create a CommonJS module object which match the interface from addon-sdk + // (addon-sdk/sources/lib/toolkit/loader.js#L678-L686) + var module = Cu.cloneInto(Object.create(null, { + id: { enumerable: true, value: id }, + uri: { enumerable: true, value: uri }, + exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) } + }), this._sandbox); + + // create a console API object + let directorScriptConsole = new PlainTextConsole(null, this._innerId); + + // inject CommonJS module globals into the sandbox prototype + Object.defineProperties(this._proto, { + module: { enumerable: true, value: module }, + exports: { enumerable: true, value: module.exports }, + console: { + enumerable: true, + value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true }) + } + }); + + Object.defineProperties(this._sandbox, { + require: { + enumerable: true, + value: Cu.cloneInto(function() { + throw Error("NOT IMPLEMENTED"); + }, this._sandbox, { cloneFunctions: true }) + } + }); + + // evaluate the director script source in the sandbox + evaluate(this._sandbox, this._scriptCode, this._scriptId); + + // prepare the messageport connected to the debugger client + let { port1, port2 } = new this._window.MessageChannel(); + + // prepare the unload callbacks queue + var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = []; + + // create the attach options + var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox); + Object.defineProperties(attachOptions, { + port: { enumerable: true, value: port1 }, + window: { enumerable: true, value: window }, + scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) }, + onUnload: { + enumerable: true, + value: Cu.cloneInto(function (cb) { + // collect unload callbacks + if (typeof cb == "function") { + sandboxOnUnloadQueue.push(cb); + } + }, this._sandbox, { cloneFunctions: true }) + } + }); + + // select the attach method + var exports = this._proto.module.exports; + if ("attachMethod" in this._scriptOptions) { + this._sandboxOnAttach = exports[this._scriptOptions.attachMethod]; + } else { + this._sandboxOnAttach = exports; + } + + if (typeof this._sandboxOnAttach !== "function") { + throw Error("the configured attachMethod '" + + (this._scriptOptions.attachMethod || "module.exports") + + "' is not exported by the directorScript"); + } + + // call the attach method + this._sandboxOnAttach.call(this._sandbox, attachOptions); + + return port2; + }, + destroy: function(onError) { + // evaluate queue unload methods if any + while(this._sandboxOnUnloadQueue.length > 0) { + let cb = this._sandboxOnUnloadQueue.pop(); + + try { + cb(); + } catch(e) { + console.error("Exception on DirectorScript Sandbox destroy", e); + onError(e); + } + } + + Cu.nukeSandbox(this._sandbox); + } +}); + +function getWindowID(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; +}
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/actors/director-registry.js @@ -0,0 +1,295 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 protocol = require("devtools/server/protocol"); +const { method, Arg, Option, RetVal } = protocol; + +const {DebuggerServer} = require("devtools/server/main"); + +/** + * Error Messages + */ + +const ERR_DIRECTOR_INSTALL_TWICE = "Trying to install a director-script twice"; +const ERR_DIRECTOR_INSTALL_EMPTY = "Trying to install an empty director-script"; +const ERR_DIRECTOR_UNINSTALL_UNKNOWN = "Trying to uninstall an unkown director-script"; + +const ERR_DIRECTOR_PARENT_UNKNOWN_METHOD = "Unknown parent process method"; +const ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD = "Unexpected call to notImplemented method"; +const ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES = "Unexpected multiple replies to called parent method"; +const ERR_DIRECTOR_CHILD_NO_REPLY = "Unexpected no reply to called parent method"; + +/** + * Director Registry + */ + +// Map of director scripts ids to director script definitions +var gDirectorScripts = Object.create(null); + +const DirectorRegistry = exports.DirectorRegistry = { + /** + * Register a Director Script with the debugger server. + * @param id string + * The ID of a director script. + * @param directorScriptDef object + * The definition of a director script. + */ + install: function (id, scriptDef) { + if (id in gDirectorScripts) { + console.error(ERR_DIRECTOR_INSTALL_TWICE,id); + return false; + } + + if (!scriptDef) { + console.error(ERR_DIRECTOR_INSTALL_EMPTY, id); + return false; + } + + gDirectorScripts[id] = scriptDef; + + return true; + }, + + /** + * Unregister a Director Script with the debugger server. + * @param id string + * The ID of a director script. + */ + uninstall: function(id) { + if (id in gDirectorScripts) { + delete gDirectorScripts[id]; + + return true; + } + + console.error(ERR_DIRECTOR_UNINSTALL_UNKNOWN, id); + + return false; + }, + + /** + * Returns true if a director script id has been registered. + * @param id string + * The ID of a director script. + */ + checkInstalled: function (id) { + return (this.list().indexOf(id) >= 0); + }, + + /** + * Returns a registered director script definition by id. + * @param id string + * The ID of a director script. + */ + get: function(id) { + return gDirectorScripts[id]; + }, + + /** + * Returns an array of registered director script ids. + */ + list: function() { + return Object.keys(gDirectorScripts); + }, + + /** + * Removes all the registered director scripts. + */ + clear: function() { + gDirectorScripts = Object.create(null); + } +}; + +/** + * E10S parent/child setup helpers + */ + +let gTrackedMessageManager = new Set(); + +exports.setupParentProcess = function setupParentProcess({mm, childID}) { + // prevents multiple subscriptions on the same messagemanager + if (gTrackedMessageManager.has(mm)) { + return; + } + gTrackedMessageManager.add(mm); + + // listen for director-script requests from the child process + mm.addMessageListener("debug:director-registry-request", handleChildRequest); + + DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected); + + /* parent process helpers */ + + function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) { + // filter out not subscribed message managers + if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) { + return; + } + + gTrackedMessageManager.delete(mm); + + // unregister for director-script requests handlers from the parent process (if any) + mm.removeMessageListener("debug:director-registry-request", handleChildRequest); + } + + function handleChildRequest(msg) { + switch (msg.json.method) { + case "get": + return DirectorRegistry.get(msg.json.args[0]); + case "list": + return DirectorRegistry.list(); + default: + console.error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD, msg.json.method); + throw new Error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD); + } + } +}; + +// skip child setup if this actor module is not running in a child process +if (DebuggerServer.isInChildProcess) { + setupChildProcess(); +} + +function setupChildProcess() { + const { sendSyncMessage } = DebuggerServer.parentMessageManager; + + DebuggerServer.setupInParent({ + module: "devtools/server/actors/director-registry", + setupParent: "setupParentProcess" + }); + + DirectorRegistry.install = notImplemented.bind(null, "install"); + DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall"); + DirectorRegistry.clear = notImplemented.bind(null, "clear"); + + DirectorRegistry.get = callParentProcess.bind(null, "get"); + DirectorRegistry.list = callParentProcess.bind(null, "list"); + + /* child process helpers */ + + function notImplemented(method) { + console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method); + throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD); + } + + function callParentProcess(method, ...args) { + var reply = sendSyncMessage("debug:director-registry-request", { + method: method, + args: args + }); + + if (reply.length === 0) { + console.error(ERR_DIRECTOR_CHILD_NO_REPLY); + throw Error(ERR_DIRECTOR_CHILD_NO_REPLY); + } else if (reply.length > 1) { + console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES); + throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES); + } + + return reply[0]; + }; +}; + +/** + * The DirectorRegistry Actor is a global actor which manages install/uninstall of + * director scripts definitions. + */ +const DirectorRegistryActor = exports.DirectorRegistryActor = protocol.ActorClass({ + typeName: "director-registry", + + /* init & destroy methods */ + initialize: function(conn, parentActor) { + protocol.Actor.prototype.initialize.call(this, conn); + }, + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this.finalize(); + }, + + finalize: method(function() { + // nothing to cleanup + }, { + oneway: true + }), + + /** + * Install a new director-script definition. + * + * @param String id + * The director-script definition identifier. + * @param String scriptCode + * The director-script javascript source. + * @param Object scriptOptions + * The director-script option object. + */ + install: method(function(id, { scriptCode, scriptOptions }) { + // TODO: add more checks on id format? + if (!id || id.length === 0) { + throw Error("director-script id is mandatory"); + } + + if (!scriptCode) { + throw Error("director-script scriptCode is mandatory"); + } + + return DirectorRegistry.install(id, { + scriptId: id, + scriptCode: scriptCode, + scriptOptions: scriptOptions + }); + }, { + request: { + scriptId: Arg(0, "string"), + scriptCode: Option(1, "string"), + scriptOptions: Option(1, "nullable:json") + }, + response: { + success: RetVal("boolean") + } + }), + + /** + * Uninstall a director-script definition. + * + * @param String id + * The identifier of the director-script definition to be removed + */ + uninstall: method(function (id) { + return DirectorRegistry.uninstall(id); + }, { + request: { + scritpId: Arg(0, "string") + }, + response: { + success: RetVal("boolean") + } + }), + + /** + * Retrieves the list of installed director-scripts. + */ + list: method(function () { + return DirectorRegistry.list(); + }, { + response: { + directorScripts: RetVal("array:string") + } + }) +}); + +/** + * The corresponding Front object for the DirectorRegistryActor. + */ +exports.DirectorRegistryFront = protocol.FrontClass(DirectorRegistryActor, { + initialize: function(client, { directorRegistryActor }) { + protocol.Front.prototype.initialize.call(this, client, { + actor: directorRegistryActor + }); + this.manage(this); + } +});
--- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -153,16 +153,18 @@ RootActor.prototype = { // Whether the style rule actor implements the modifySelector method // that modifies the rule's selector selectorEditable: true, // Whether the page style actor implements the addNewRule method that // adds new rules to the page addNewRule: true, // Whether the dom node actor implements the getUniqueSelector method getUniqueSelector: true, + // Whether the director scripts are supported + directorScripts: true, // Whether the debugger server supports // blackboxing/pretty-printing (not supported in Fever Dream yet) noBlackBoxing: false, noPrettyPrinting: false, // Whether the page style actor implements the getUsedFontFaces method // that returns the font faces used on a node getUsedFontFaces: true },
--- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -234,34 +234,67 @@ var PageStyleActor = protocol.ActorClass filter: Option(1, "string"), }, response: { computed: RetVal("json") } }), /** + * Get all the fonts from a page. + * + * @param object options + * `includePreviews`: Whether to also return image previews of the fonts. + * `previewText`: The text to display in the previews. + * `previewFontSize`: The font size of the text in the previews. + * + * @returns object + * object with 'fontFaces', a list of fonts that apply to this node. + */ + getAllUsedFontFaces: method(function(options) { + let windows = this.inspector.tabActor.windows; + let fontsList = []; + for(let win of windows){ + fontsList = [...fontsList, + ...this.getUsedFontFaces(win.document.body, options)]; + } + return fontsList; + }, + { + request: { + includePreviews: Option(0, "boolean"), + previewText: Option(0, "string"), + previewFontSize: Option(0, "string"), + previewFillStyle: Option(0, "string") + }, + response: { + fontFaces: RetVal("array:fontface") + } + }), + + /** * Get the font faces used in an element. * - * @param NodeActor node + * @param NodeActor node / actual DOM node * The node to get fonts from. * @param object options * `includePreviews`: Whether to also return image previews of the fonts. * `previewText`: The text to display in the previews. * `previewFontSize`: The font size of the text in the previews. * * @returns object * object with 'fontFaces', a list of fonts that apply to this node. */ getUsedFontFaces: method(function(node, options) { - let contentDocument = node.rawNode.ownerDocument; - + // node.rawNode is defined for NodeActor objects + let actualNode = node.rawNode || node; + let contentDocument = actualNode.ownerDocument; // We don't get fonts for a node, but for a range let rng = contentDocument.createRange(); - rng.selectNodeContents(node.rawNode); + rng.selectNodeContents(actualNode); let fonts = DOMUtils.getUsedFontFaces(rng); let fontsArray = []; for (let i = 0; i < fonts.length; i++) { let font = fonts.item(i); let fontFace = { name: font.name, CSSFamilyName: font.CSSFamilyName,
--- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -384,16 +384,21 @@ var DebuggerServer = { constructor: "WebappsActor", type: { global: true } }); this.registerModule("devtools/server/actors/device", { prefix: "device", constructor: "DeviceActor", type: { global: true } }); + this.registerModule("devtools/server/actors/director-registry", { + prefix: "directorRegistry", + constructor: "DirectorRegistryActor", + type: { global: true } + }); }, /** * Install tab actors in documents loaded in content childs */ addChildActors: function () { // In case of apps being loaded in parent process, DebuggerServer is already // initialized and browser actors are already loaded, @@ -499,16 +504,21 @@ var DebuggerServer = { constructor: "MonitorActor", type: { global: true, tab: true } }); this.registerModule("devtools/server/actors/timeline", { prefix: "timeline", constructor: "TimelineActor", type: { global: true, tab: true } }); + this.registerModule("devtools/server/actors/director-manager", { + prefix: "directorManager", + constructor: "DirectorManagerActor", + type: { global: false, tab: true } + }); if ("nsIProfiler" in Ci) { this.registerModule("devtools/server/actors/profiler", { prefix: "profiler", constructor: "ProfilerActor", type: { global: true, tab: true } }); } this.registerModule("devtools/server/actors/animation", {
--- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -37,16 +37,18 @@ EXTRA_JS_MODULES.devtools.server.actors 'actors/animation.js', 'actors/call-watcher.js', 'actors/canvas.js', 'actors/child-process.js', 'actors/childtab.js', 'actors/common.js', 'actors/csscoverage.js', 'actors/device.js', + 'actors/director-manager.js', + 'actors/director-registry.js', 'actors/eventlooplag.js', 'actors/framerate.js', 'actors/gcli.js', 'actors/highlighter.js', 'actors/inspector.js', 'actors/layout.js', 'actors/memory.js', 'actors/monitor.js',
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini +++ b/toolkit/devtools/server/tests/mochitest/chrome.ini @@ -1,10 +1,12 @@ [DEFAULT] support-files = + director-helpers.js + director-script-target.html inspector-helpers.js inspector-styles-data.css inspector-styles-data.html inspector-traversal-data.html nonchrome_unsafeDereference.html inspector_getImageData.html large-image.jpg memory-helpers.js @@ -68,10 +70,13 @@ skip-if = buildapp == 'mulet' [test_memory_allocations_05.html] [test_memory_attach_01.html] [test_memory_attach_02.html] [test_memory_census.html] [test_memory_gc_01.html] [test_preference.html] [test_connectToChild.html] skip-if = buildapp == 'mulet' +[test_director.html] +[test_director_connectToChild.html] ++skip-if = buildapp == 'mulet' [test_attachProcess.html] skip-if = buildapp == 'mulet'
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/director-helpers.js @@ -0,0 +1,100 @@ +var Cu = Components.utils; +Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); +Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); + +const Services = devtools.require("Services"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true); + +SimpleTest.registerCleanupFunction(function() { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled"); +}); + +const {Class} = devtools.require("sdk/core/heritage"); + +const {promiseInvoke} = devtools.require("devtools/async-utils"); + +const { DirectorRegistry, + DirectorRegistryFront } = devtools.require("devtools/server/actors/director-registry"); + +const { DirectorManagerFront } = devtools.require("devtools/server/actors/director-manager"); +const protocol = devtools.require("devtools/server/protocol"); + +const {Task} = devtools.require("resource://gre/modules/Task.jsm"); + +/*********************************** + * director helpers functions + **********************************/ + +function waitForEvent(target, name) { + return new Promise((resolve, reject) => { + target.once(name, (...args) => { resolve(args); }); + }); +} + +function* newConnectedDebuggerClient(opts) { + var transport = DebuggerServer.connectPipe(); + var client = new DebuggerClient(transport); + + yield promiseInvoke(client, client.connect); + + var root = yield promiseInvoke(client, client.listTabs); + + return { + client: client, + root: root, + transport: transport + }; +} + +function* installTestDirectorScript(client, root, scriptId, scriptDefinition) { + var directorRegistryClient = new DirectorRegistryFront(client, root); + + yield directorRegistryClient.install(scriptId, scriptDefinition); + + directorRegistryClient.destroy(); +} + +function* getTestDirectorScript(manager, tab, scriptId) { + var directorScriptClient = yield manager.getByScriptId(scriptId); + return directorScriptClient; +} + +function purgeInstalledDirectorScripts() { + DirectorRegistry.clear(); +} + +function* installDirectorScriptAndWaitAttachOrError({client, root, manager, + scriptId, scriptDefinition}) { + yield installTestDirectorScript(client, root, scriptId, scriptDefinition); + + var selectedTab = root.tabs[root.selected]; + var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, scriptId); + + var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach"); + var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error"); + + testDirectorScriptClient.setup({reload: false}); + + var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach, + waitForDirectorScriptError]); + + testDirectorScriptClient.finalize(); + + return receivedEvent; +} + +function assertIsDirectorScriptError(error) { + ok(!!error, "received error should be defined"); + ok(!!error.message, "errors should contain a message"); + ok(!!error.stack, "errors should contain a stack trace"); + ok(!!error.fileName, "errors should contain a fileName"); + ok(typeof error.columnNumber == "number", "errors should contain a columnNumber"); + ok(typeof error.lineNumber == "number", "errors should contain a lineNumber"); + + ok(!!error.directorScriptId, "errors should contain a directorScriptId"); +}
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/director-script-target.html @@ -0,0 +1,15 @@ +<html> + <head> + <script> + // change the eval function to ensure the window object in the debug-script is correctly wrapped + window.eval = function () { + return "unsecure-eval-called"; + }; + + var globalAccessibleVar = "global-value"; + </script> + </head> + <body> + <h1>debug script target</h1> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_director.html @@ -0,0 +1,479 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> + <script type="application/javascript;version=1.8" src="./director-helpers.js"></script> + <script type="application/javascript;version=1.8"> +window.onload = function() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + var tests = [ + runDirectorScriptModuleExports, + runDirectorScriptErrorOnNoAttachExports, + runDirectorScriptErrorOnLoadTest, + runDirectorScriptErrorOnRequire, + runDirectorScriptErrorOnUnloadTest, + runDirectorScriptSetupAndReceiveMessagePortTest, + runDirectorEnableDirectorScriptsTest, + runDirectorScriptDetachEventTest, + runDirectorScriptWindowEval + ].map((testCase) => { + return function* () { + setup(); + yield testCase().then(null, (e) => { + console.error("Exception during testCase run", e); + ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t")); + }); + + teardown(); + }; + }); + + for (var test of tests) { + yield test(); + } + }).then( + function success() { + SimpleTest.finish() + }, + function error(e) { + console.error("Exception during testCase run", e); + ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t")); + + SimpleTest.finish(); + } + ); +}; + +var targetWin = null; + +function setup() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + + SimpleTest.registerCleanupFunction(teardown); + } +} + +function teardown() { + purgeInstalledDirectorScripts(); + + DebuggerServer.destroy(); + if (targetWin) { + targetWin.close(); + } +} + +/*********************************** + * test cases + **********************************/ + +function runDirectorScriptModuleExports() { + targetWin = window.open("about:blank"); + + var testDirectorScriptModuleExports = { + scriptCode: "(" + (function() { + module.exports = function() {}; + }).toString() + ")();", + scriptOptions: {} + } + + var testDirectorScriptAttachMethodOption = { + scriptCode: "(" + (function() { + exports.attach = function() {}; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + var selectedTab = root.tabs[root.selected]; + var manager = new DirectorManagerFront(client, selectedTab); + + var receivedEvent1 = yield installDirectorScriptAndWaitAttachOrError({ + client: client, root: root, manager: manager, + scriptId: "testDirectorscriptModuleExports", + scriptDefinition: testDirectorScriptModuleExports + }); + ok(!!receivedEvent1.port, "received attach from testDirectorScriptModuleExports"); + + var receivedEvent2 = yield installDirectorScriptAndWaitAttachOrError({ + client: client, root: root, manager: manager, + scriptId: "testDirectorscriptAttachMethodOption", + scriptDefinition: testDirectorScriptModuleExports + }); + ok(!!receivedEvent2.port, "received attach event from testDirectorScriptAttachMethodOption"); + + client.close(); + }) +} + +function runDirectorScriptErrorOnNoAttachExports() { + targetWin = window.open("about:blank"); + + var testDirectorScriptRaiseErrorOnNoAttachExports = { + scriptCode: "(" + (function() { + // this director script should raise an error + // because it doesn't export any attach method + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + var selectedTab = root.tabs[root.selected]; + var manager = new DirectorManagerFront(client, selectedTab); + + var error = yield installDirectorScriptAndWaitAttachOrError({ + client: client, root: root, manager: manager, + scriptId: "testDirectorscriptRaiseErrorOnNoAttachExports", + scriptDefinition: testDirectorScriptRaiseErrorOnNoAttachExports + }); + + assertIsDirectorScriptError(error); + + client.close(); + }); +} + +function runDirectorScriptErrorOnRequire() { + targetWin = window.open("about:blank"); + + var testDirectorScriptRaiseErrorOnRequire = { + scriptCode: "(" + (function() { + // this director script should raise an error + // because require raise a "not implemented" exception + console.log("PROVA", this) + require("fake_module"); + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + var selectedTab = root.tabs[root.selected]; + var manager = new DirectorManagerFront(client, selectedTab); + + var error = yield installDirectorScriptAndWaitAttachOrError({ + client: client, root: root, manager: manager, + scriptId: "testDirectorscriptRaiseErrorOnRequire", + scriptDefinition: testDirectorScriptRaiseErrorOnRequire + }); + + assertIsDirectorScriptError(error); + is(error.message, "Error: NOT IMPLEMENTED", "error message should contains the expected error message"); + client.close(); + }); +} + +function runDirectorScriptErrorOnLoadTest() { + targetWin = window.open("about:blank"); + + var testDirectorScriptRaiseErrorOnLoad = { + scriptCode: "(" + (function() { + // this will raise an exception on evaluating + // the director script + raise.an_error.during.content_script.load(); + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptRaiseErrorOnLoad); + + var selectedTab = root.tabs[root.selected]; + var manager = new DirectorManagerFront(client, selectedTab); + var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript"); + + var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error"); + + // activate the director script without window reloading + testDirectorScriptClient.setup({reload: false}); + + var [error] = yield waitForDirectorScriptError; + + assertIsDirectorScriptError(error); + + client.close(); + }); +} + +function runDirectorScriptErrorOnUnloadTest() { + targetWin = window.open("about:blank"); + + var testDirectorScriptRaiseErrorOnUnload = { + scriptCode: "(" + (function() { + module.exports = function({onUnload}) { + // this will raise an exception on unload the director script + onUnload(function() { + raise_an_error_onunload(); + }); + }; + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptRaiseErrorOnUnload); + + var selectedTab = root.tabs[root.selected]; + var manager = new DirectorManagerFront(client, selectedTab); + var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript"); + var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach"); + + // activate the director script without window reloading + testDirectorScriptClient.setup({reload: false}); + + yield waitForDirectorScriptAttach; + + var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error"); + + testDirectorScriptClient.finalize(); + + var [error] = yield waitForDirectorScriptError; + + assertIsDirectorScriptError(error); + + client.close(); + }); +} + + +function runDirectorScriptSetupAndReceiveMessagePortTest() { + targetWin = window.open("about:blank"); + + var testDirectorScriptOptions = { + scriptCode: "(" + (function() { + module.exports = function({port}) { + port.onmessage = function(evt) { + // echo messages + evt.source.postMessage(evt.data); + }; + }; + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptOptions); + + var selectedTab = root.tabs[root.selected]; + + // get a testDirectorScriptClient + var manager = new DirectorManagerFront(client, selectedTab); + var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript"); + + var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach"); + + // activate the director script without window reloading + // (and wait for attach) + testDirectorScriptClient.setup({reload: false}); + + var [attachEvent] = yield waitForDirectorScriptAttach; + + // call the connectPort method to get a MessagePortClient + var port = attachEvent.port; + + ok(!!port && !!port.postMessage, "messageport actor client received"); + + // exchange messages over the MessagePort + var waitForMessagePortMessage = waitForEvent(port, "message"); + // needs to explicit start the port + port.start(); + + var msg = { k1: "v1", k2: [1, 2, 3] }; + port.postMessage(msg); + + var reply = yield waitForMessagePortMessage; + + ok(JSON.stringify(reply[0].data) === JSON.stringify(msg), + "echo reply received on the MessagePortClient"); + + yield client.close(); + }); +} + +function runDirectorEnableDirectorScriptsTest() { + targetWin = window.open("about:blank"); + + var testDirectorScriptOptions = { + scriptCode: "(" + (function() { + module.exports = function({port}) { + port.onmessage = function(evt) { + // echo messages + evt.source.postMessage(evt.data); + }; + }; + }).toString() + ")();", + scriptOptions: {} + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptOptions); + + var selectedTab = root.tabs[root.selected]; + + var tabDirectorClient = new DirectorManagerFront(client, selectedTab); + + var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach"); + + tabDirectorClient.enableByScriptIds(["*"], { reload: false }); + + var [attachEvent] = yield waitForDirectorScriptAttach; + + is(attachEvent.directorScriptId, "testDirectorScript", "attach event should contains directorScriptId"); + + yield client.close(); + }); +} + +function runDirectorScriptDetachEventTest() { + targetWin = window.open("director-script-target.html"); + + var testDirectorScriptOptions = { + scriptCode: "(" + (function() { + exports.attach = function({port, onUnload}) { + onUnload(function() { + port.postMessage("ONUNLOAD"); + }); + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptOptions); + + var selectedTab = root.tabs[root.selected]; + + // NOTE: tab needs to be attached to receive director-script-detach events + yield promiseInvoke(client, client.attachTab, selectedTab.actor); + + var tabDirectorClient = new DirectorManagerFront(client, selectedTab); + + var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach"); + var waitForDirectorScriptDetach = waitForEvent(tabDirectorClient, "director-script-detach"); + + tabDirectorClient.enableByScriptIds(["*"], {reload: true}); + + var [attachEvent] = yield waitForDirectorScriptAttach; + + // exchange messages over the MessagePort + var waitForMessagePortEvent = waitForEvent(attachEvent.port, "message"); + // needs to explicit start the port + attachEvent.port.start(); + + tabDirectorClient.disableByScriptIds(["*"], {reload: false}); + + // changing the window location should generate a director-script-detach event + var [detachEvent] = yield waitForDirectorScriptDetach; + + is(detachEvent.directorScriptId, "testDirectorScript", "detach event should contains directorScriptId"); + + var [portEvent] = yield waitForMessagePortEvent; + + is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach"); + + yield client.close(); + }); +} + +function runDirectorScriptWindowEval() { + targetWin = window.open("http://mochi.test:8888/chrome/toolkit/devtools/server/tests/mochitest/director-script-target.html"); + + var testDirectorScriptOptions = { + scriptCode: "(" + (function() { + exports.attach = function({window, port}) { + var onpageloaded = function() { + var globalVarValue = window.eval("window.globalAccessibleVar;"); + port.postMessage(globalVarValue); + }; + + if (window.document.readyState === "complete") { + onpageloaded(); + } else { + window.onload = onpageloaded; + } + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + } + + return Task.spawn(function* () { + var { client, root } = yield newConnectedDebuggerClient(); + + yield installTestDirectorScript(client, root, "testDirectorScript", + testDirectorScriptOptions); + + var selectedTab = root.tabs[root.selected]; + + // NOTE: tab needs to be attached to receive director-script-detach events + yield promiseInvoke(client, client.attachTab, selectedTab.actor); + + var tabDirectorClient = new DirectorManagerFront(client, selectedTab); + + var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach"); + var waitForDirectorScriptError = waitForEvent(tabDirectorClient, "director-script-error"); + + tabDirectorClient.enableByScriptIds(["*"], {reload: false}); + + var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach, + waitForDirectorScriptError]); + + ok(!!receivedEvent.port, "received director-script-attach"); + + // exchange messages over the MessagePort + var waitForMessagePortEvent = waitForEvent(receivedEvent.port, "message"); + // needs to explicit start the port + receivedEvent.port.start(); + + var [portEvent] = yield waitForMessagePortEvent; + + ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe"); + + is(portEvent.data, "global-value", "window.globalAccessibleVar should be accessible through window.eval"); + + yield client.close(); + }); +} + + </script> +</pre> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_director_connectToChild.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> + <script type="application/javascript;version=1.8" src="./director-helpers.js"></script> + <script type="application/javascript;version=1.8"> +window.onload = function() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + var tests = [ + runPropagateDirectorScriptsToChildTest, + ].map((testCase) => { + return function* () { + setup(); + yield testCase().then(null, (e) => { + ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t")); + }); + + teardown(); + }; + }); + + for (var test of tests) { + yield test(); + } + + SimpleTest.finish(); + }); +}; + +function setup() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + SimpleTest.registerCleanupFunction(function() { + DebuggerServer.destroy(); + }); + } +} + +function teardown() { + purgeInstalledDirectorScripts(); + DebuggerServer.destroy(); +} + +/*********************************** + * test cases + **********************************/ + +function runPropagateDirectorScriptsToChildTest() { + let iframe = document.createElement("iframe"); + iframe.mozbrowser = true; + + document.body.appendChild(iframe); + + return Task.spawn(function* () { + var { client, root, transport } = yield newConnectedDebuggerClient(); + + var directorRegistryClient = new DirectorRegistryFront(client, root); + + // install a director script + yield directorRegistryClient.install("testPropagatedDirectorScript", { + scriptCode: "console.log('director script test');", + scriptOptions: {} + }); + + var conn = transport._serverConnection; + var childActor = yield DebuggerServer.connectToChild(conn, iframe); + + ok(typeof childActor.directorManagerActor !== "undefined", + "childActor.directorActor should be defined"); + + var childDirectorManagerClient = new DirectorManagerFront(client, childActor); + + var directorScriptList = yield childDirectorManagerClient.list(); + + ok(directorScriptList.installed.length === 1 && + directorScriptList.installed[0] === "testPropagatedDirectorScript", + "director scripts propagated correctly") + + yield client.close(); + }); +} + </script> +</pre> +</body> +</html>