blob: a927ec6fa4df65aaac70cf4775246fde03bf87a2 [file] [log] [blame]
/* Copyright 2015 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
/**
* @fileoverview
* The application side of the application/cloud print dialog interface, used
* by the application to exchange messages with the dialog.
*/
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* According to https://developer.chrome.com/apps/tags/webview, the level
* ranges from 0 to 4. But in real life they are -1 (debug), 0 (log and info),
* 1 (warn), and 2 (error). See crbug.com/499408
*
* @enum {number}
*/
remoting.ConsoleMessageLevel = {
// console.debug
VERBOSE: -1,
// console.info or console.log
INFO: 0,
// console.warn
WARNING: 1,
//console.err
ERROR: 2
};
(function() {
'use strict';
/**
* Interval to refresh the access token used by the cloud print dialog.
* Refreshing the token every 30 minutes should be good enough because the
* access token will be valid for an hour.
*
* @const {number}
*/
var CLOUD_PRINT_DIALOG_TOKEN_REFRESH_INTERVAL_MS = 30 * 60 * 1000;
/**
* @param {!Webview} webview The webview hosting the cloud cloud print dialog.
* @param {remoting.WindowShape} windowShape
* @param {base.WindowMessageDispatcher} windowMessageDispatcher
* @param {!remoting.ConnectedView} connectedView
* @constructor
* @implements {remoting.WindowShape.ClientUI}
* @implements {base.Disposable}
*/
remoting.CloudPrintDialogContainer =
function(webview, windowShape, windowMessageDispatcher, connectedView) {
/** @private {!Webview} */
this.webview_ = webview;
/** @private {remoting.WindowShape} */
this.windowShape_ = windowShape;
/** @private {!remoting.ConnectedView} */
this.connectedView_ = connectedView;
// TODO (weitaosu): This is only needed if the cloud print webview is on the
// same page as the plugin. We should remove it if we move the webview to a
// standalone message window.
this.connectedView_.allowFocus(webview);
/** @private {string} */
this.accessToken_ = '';
/** @private {base.WindowMessageDispatcher} */
this.windowMessageDispatcher_ = windowMessageDispatcher;
this.windowMessageDispatcher_.registerMessageHandler(
'cloud-print-dialog', this.onMessage_.bind(this));
/** @private {base.RepeatingTimer} */
this.timer_ = new base.RepeatingTimer(
this.cacheAccessToken_.bind(this),
CLOUD_PRINT_DIALOG_TOKEN_REFRESH_INTERVAL_MS, true);
// Adding a synchronous (blocking) handler so that the reqeust headers can
// be modified on the spot.
webview.request.onBeforeSendHeaders.addListener(
this.setAuthHeaders_.bind(this),
/** @type {!RequestFilter} */ ({urls: ['https://*.google.com/*']}),
['requestHeaders','blocking']);
};
remoting.CloudPrintDialogContainer.INJECTED_SCRIPT =
'_modules/koejkfhmphamcgafjmkellhnekdkopod/cloud_print_dialog_injected.js';
remoting.CloudPrintDialogContainer.CLOUD_PRINT_DIALOG_URL =
'https://www.google.com/cloudprint/dialog.html?';
remoting.CloudPrintDialogContainer.prototype.dispose = function() {
this.windowMessageDispatcher_.unregisterMessageHandler('cloud-print-dialog');
this.timer_.dispose();
};
/**
* Timer callback to cache the access token.
* @private
*/
remoting.CloudPrintDialogContainer.prototype.cacheAccessToken_ = function() {
/** @type {remoting.CloudPrintDialogContainer} */
var that = this;
remoting.identity.getNewToken().then(
function(/** string */ token){
console.assert(token !== that.accessToken_);
that.accessToken_ = token;
}).catch(remoting.Error.handler(function(/** remoting.Error */ error) {
console.log('Failed to refresh access token: ' + error.toString());
}));
};
/**
* @param {Array<{left: number, top: number, width: number, height: number}>}
* rects List of rectangles.
*/
remoting.CloudPrintDialogContainer.prototype.addToRegion = function(rects) {
var rect =
/** @type {ClientRect} */(this.webview_.getBoundingClientRect());
rects.push({left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height});
};
/**
* Show the cloud print dialog.
*/
remoting.CloudPrintDialogContainer.prototype.showCloudPrintUI = function() {
// TODO (weitaosu): Considering showing the cloud print dialog in a separate
// window or using remoting.Html5ModalDialog to show the modal dialog.
this.webview_.hidden = false;
this.windowShape_.registerClientUI(this);
this.windowShape_.centerToDesktop(this.webview_);
};
/**
* Hide the cloud print dialog.
*/
remoting.CloudPrintDialogContainer.prototype.hideCloudPrintUI = function() {
this.webview_.hidden = true;
this.windowShape_.unregisterClientUI(this);
this.connectedView_.returnFocusToPlugin();
}
/**
* Event handler to process messages from the webview.
*
* @param {Event} event
* @private
*/
remoting.CloudPrintDialogContainer.prototype.onMessage_ = function(event) {
var data = event.data;
console.assert(typeof data === 'object' &&
data['source'] == 'cloud-print-dialog');
switch (event.data['command']) {
case 'cp-dialog-on-init::':
// We actually never receive this message because the cloud print dialog
// has already been initialized by the time the injected script finishes
// executing.
break;
case 'cp-dialog-on-close::':
this.hideCloudPrintUI();
break;
default:
console.error('Unexpected message:', event.data['command'], event.data);
}
};
/**
* Retrieve the file specified by |fileName| in the app package and pass its
* content to the onDone callback.
*
* @param {string} fileName Name of the file in the app package to be read.
* @param {!function(string):void} onDone Callback to be invoked on success.
* @param {!function(*):void} onError Callback to be invoked on failure.
*/
remoting.CloudPrintDialogContainer.readFile =
function(fileName, onDone, onError) {
var fileUrl = chrome.runtime.getURL(fileName);
var xhr = new remoting.Xhr({ method: 'GET', url: fileUrl});
xhr.start().then(function(/** !remoting.Xhr.Response */ response) {
if (response.status == 200) {
onDone(response.getText());
} else {
onError('xhr.status = ' + response.status);
}
});
};
/**
* Lanunch the cloud print dialog to print the document.
*
* @param {string} title Title of the print job.
* @param {string} type Type of the storage of the document (url, gdrive, etc).
* @param {string} data Meaning of this field depends on the |type| parameter.
*/
remoting.CloudPrintDialogContainer.prototype.printDocument =
function(title, type, data) {
var dialogUrl = remoting.CloudPrintDialogContainer.CLOUD_PRINT_DIALOG_URL;
dialogUrl += 'title=' + encodeURIComponent(title) + '&';
switch (type) {
case 'url':
// 'data' should contain the url to the document to be printed.
if (data.substr(0, 7) !== 'http://' && data.substr(0, 8) !== 'https://') {
console.error('Bad URL: ' + data);
return;
}
dialogUrl += 'type=url&url=' + encodeURIComponent(data);
break;
case 'google.drive':
// 'data' should contain the doc id of the gdrive document to be printed.
dialogUrl += 'type=google.drive&content=' + encodeURIComponent(data);
break;
default:
console.error('Unknown content type for the printDocument command.');
return;
}
// TODO (weitaosu); Consider moving the event registration to the ctor
// and add unregistration in the dtor.
var that = this;
/** @param {string} script */
var showDialog = function(script) {
/** @type {Webview} */
var webview = that.webview_;
var sendHandshake = function() {
webview.contentWindow.postMessage('app-remoting-handshake', '*');
}
/** @param {Event} event */
var redirectConsoleOutput = function(event) {
var e = /** @type {chrome.ConsoleMessageBrowserEvent} */ (event);
var message = 'console message from webviwe: {' +
'level=' + e.level + ', ' +
'source=' + e.sourceId + ', ' +
'line=' + e.line + ', ' +
'message="' + e.message + '"}';
switch (e['level']) {
case remoting.ConsoleMessageLevel.VERBOSE:
console.debug(message);
break;
case remoting.ConsoleMessageLevel.INFO:
console.info(message);
break;
case remoting.ConsoleMessageLevel.WARNING:
console.warn(message);
break;
case remoting.ConsoleMessageLevel.ERROR:
console.error(message);
break;
default:
console.error('unrecognized message level. ' + message);
break;
}
}
webview.addEventListener('consolemessage', redirectConsoleOutput);
// Inject the script and send a handshake message to the cloud print dialog
// after the injected script has been executed.
webview.addEventListener("loadstart", function(event) {
console.log('"loadstart" captured in webview containier.');
// TODO (weitaosu): Consider switching to addContentScripts when M44
// is released.
webview.executeScript(
{code: script + ' //# sourceURL=cloud_print_dialog_injected.js'},
sendHandshake);
});
// We need to show the cloud print UI here because we will never receive
// the 'cp-dialog-on-init::' message.
webview.src = dialogUrl;
that.showCloudPrintUI();
}
/** @param {*} errorMsg */
var onError = function(errorMsg) {
console.error('Failed to retrieve the script: ', errorMsg);
}
remoting.CloudPrintDialogContainer.readFile(
remoting.CloudPrintDialogContainer.INJECTED_SCRIPT, showDialog, onError);
};
/**
* Handler of the onBeforeSendHeaders event for the webview. It adds the auth
* header to the request being send. Note that this handler is synchronous so
* modifications to the requst must happen before it returns.
*
* @param {Object} details Details of the WebRequest.
* @return {!BlockingResponse} The modified request headers.
* @private
*/
remoting.CloudPrintDialogContainer.prototype.setAuthHeaders_ =
function(details) {
var url = /** @type {string} */ (details['url']);
console.log('Setting auth token for request: ', url);
var headers = /** @type {Array} */ (details['requestHeaders']) || [];
if (this.accessToken_.length == 0) {
console.error('No auth token available for the request: ', url);
return /** @type {!BlockingResponse} */ ({'requestHeaders': headers});
}
// Make fresh copy of the headers array.
var newHeaders = /** @type {Array} */ (headers.slice());
newHeaders.push({
'name': 'Authorization',
'value': 'Bearer ' + this.accessToken_
});
return /** @type {!BlockingResponse} */ ({'requestHeaders': newHeaders});
};
})();