diff options
10 files changed, 391 insertions, 12 deletions
diff --git a/chrome/browser/extensions/api/notification/notification_apitest.cc b/chrome/browser/extensions/api/notification/notification_apitest.cc index 7c9970f..eead88f 100644 --- a/chrome/browser/extensions/api/notification/notification_apitest.cc +++ b/chrome/browser/extensions/api/notification/notification_apitest.cc @@ -41,8 +41,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestIdUsage) { "[\"\", " // Empty string: ask API to generate ID "{" "\"templateType\": \"simple\"," - "\"iconUrl\": \"http://www.google.com/intl/en/chrome/assets/" - "common/images/chrome_logo_2x.png\"," + "\"iconUrl\": \"an/image/that/does/not/exist.png\"," "\"title\": \"Attention!\"," "\"message\": \"Check out Cirque du Soleil\"" "}]", @@ -67,8 +66,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestIdUsage) { "[\"" + notification_id + "\", " "{" "\"templateType\": \"simple\"," - "\"iconUrl\": \"http://www.google.com/intl/en/chrome/assets/" - "common/images/chrome_logo_2x.png\"," + "\"iconUrl\": \"an/image/that/does/not/exist.png\"," "\"title\": \"Attention!\"," "\"message\": \"Too late! The show ended yesterday\"" "}]", @@ -100,8 +98,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestIdUsage) { "[\"xxxxxxxxxxxx\", " "{" "\"templateType\": \"simple\"," - "\"iconUrl\": \"http://www.google.com/intl/en/chrome/assets/" - "common/images/chrome_logo_2x.png\"," + "\"iconUrl\": \"an/image/that/does/not/exist.png\"," "\"title\": \"!\"," "\"message\": \"!\"" "}]", @@ -166,8 +163,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestBaseFormatNotification) { "[\"\", " "{" "\"templateType\": \"basic\"," - "\"iconUrl\": \"http://www.google.com/intl/en/chrome/assets/" - "common/images/chrome_logo_2x.png\"," + "\"iconUrl\": \"an/image/that/does/not/exist.png\"," "\"title\": \"Attention!\"," "\"message\": \"Check out Cirque du Soleil\"," "\"priority\": 1," @@ -206,7 +202,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestMultipleItemNotification) { "[\"\", " "{" "\"templateType\": \"list\"," - "\"iconUrl\": \"https://code.google.com/p/chromium/logo\"," + "\"iconUrl\": \"an/image/that/does/not/exist.png\"," "\"title\": \"Multiple Item Notification Title\"," "\"message\": \"Multiple item notification message.\"," "\"items\": [" @@ -236,3 +232,7 @@ IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestMultipleItemNotification) { IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestEvents) { ASSERT_TRUE(RunExtensionTest("notification/api/events")) << message_; } + +IN_PROC_BROWSER_TEST_F(NotificationApiTest, TestCSP) { + ASSERT_TRUE(RunExtensionTest("notification/api/csp")) << message_; +} diff --git a/chrome/chrome_renderer.gypi b/chrome/chrome_renderer.gypi index b240bce..aa48477 100644 --- a/chrome/chrome_renderer.gypi +++ b/chrome/chrome_renderer.gypi @@ -168,6 +168,7 @@ 'renderer/resources/extensions/json_schema.js', 'renderer/resources/extensions/last_error.js', 'renderer/resources/extensions/miscellaneous_bindings.js', + 'renderer/resources/extensions/notification_custom_bindings.js', 'renderer/resources/extensions/omnibox_custom_bindings.js', 'renderer/resources/extensions/page_action_custom_bindings.js', 'renderer/resources/extensions/page_actions_custom_bindings.js', diff --git a/chrome/common/extensions/api/experimental_notification.idl b/chrome/common/extensions/api/experimental_notification.idl index a59c630..2c43dc0 100644 --- a/chrome/common/extensions/api/experimental_notification.idl +++ b/chrome/common/extensions/api/experimental_notification.idl @@ -35,7 +35,6 @@ TemplateType templateType; // Sender's avatar, app icon, or a thumbnail for image notifications. - // TODO(miket): see browserAction.setIcon() for a better approach. DOMString iconUrl; // Title of the notification (e.g. sender name for email). diff --git a/chrome/renderer/extensions/dispatcher.cc b/chrome/renderer/extensions/dispatcher.cc index dcd161f..a76441f 100644 --- a/chrome/renderer/extensions/dispatcher.cc +++ b/chrome/renderer/extensions/dispatcher.cc @@ -632,6 +632,7 @@ void Dispatcher::PopulateSourceMap() { // Libraries. source_map_.RegisterSource("contentWatcher", IDR_CONTENT_WATCHER_JS); + source_map_.RegisterSource("imageUtil", IDR_IMAGE_UTIL_JS); source_map_.RegisterSource("lastError", IDR_LAST_ERROR_JS); source_map_.RegisterSource("schemaUtils", IDR_SCHEMA_UTILS_JS); source_map_.RegisterSource("sendRequest", IDR_SEND_REQUEST_JS); @@ -659,6 +660,8 @@ void Dispatcher::PopulateSourceMap() { IDR_EXPERIMENTAL_MEDIA_GALLERIES_CUSTOM_BINDINGS_JS); source_map_.RegisterSource("experimental.offscreen", IDR_EXPERIMENTAL_OFFSCREENTABS_CUSTOM_BINDINGS_JS); + source_map_.RegisterSource("experimental.notification", + IDR_NOTIFICATION_CUSTOM_BINDINGS_JS); source_map_.RegisterSource("extension", IDR_EXTENSION_CUSTOM_BINDINGS_JS); source_map_.RegisterSource("fileBrowserHandler", IDR_FILE_BROWSER_HANDLER_CUSTOM_BINDINGS_JS); diff --git a/chrome/renderer/resources/extensions/image_util.js b/chrome/renderer/resources/extensions/image_util.js new file mode 100644 index 0000000..4f418e2 --- /dev/null +++ b/chrome/renderer/resources/extensions/image_util.js @@ -0,0 +1,69 @@ +// Copyright 2013 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. + +// This function takes an object |imageSpec| with the key |path| - +// corresponding to the internet URL to be translated - and optionally +// |width| and |height| which are the maximum dimensions to be used when +// converting the image. +function loadDataUrl(imageSpec, callbacks) { + var path = imageSpec.path; + var img = new Image(); + if (typeof callbacks.onerror === 'function') { + img.onerror = function() { + callbacks.onerror({ problem: 'could_not_load', path: path }); + }; + } + img.onload = function() { + var canvas = document.createElement('canvas'); + + canvas.width = img.width; + if (imageSpec.width && imageSpec.width < img.width) + canvas.width = imageSpec.width; + canvas.height = img.height; + if (imageSpec.height && imageSpec.height < img.height) + canvas.height = imageSpec.height; + + var canvas_context = canvas.getContext('2d'); + canvas_context.clearRect(0, 0, canvas.width, canvas.height); + canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); + try { + var dataUrl = canvas.toDataURL(); + if (typeof callbacks.oncomplete === 'function') { + callbacks.oncomplete(dataUrl); + } + } catch (e) { + if (typeof callbacks.onerror === 'function') { + callbacks.onerror({ problem: 'data_url_unavailable', path: path }); + } + } + } + img.src = path; +} + +function on_complete_index(index, err, loading, finished, callbacks) { + return function(imageData) { + delete loading[index]; + finished[index] = imageData; + if (err) + callbacks.onerror(index); + if (Object.keys(loading).length == 0) + callbacks.oncomplete(finished); + } +} + +function loadAllImages(imageSpecs, callbacks) { + var loading = {}, finished = [], + index, pathname; + + for (var index = 0; index < imageSpecs.length; index++) { + loading[index] = imageSpecs[index]; + loadDataUrl(imageSpecs[index], { + oncomplete: on_complete_index(index, false, loading, finished, callbacks), + onerror: on_complete_index(index, true, loading, finished, callbacks) + }); + } +} + +exports.loadDataUrl = loadDataUrl; +exports.loadAllImages = loadAllImages; diff --git a/chrome/renderer/resources/extensions/notification_custom_bindings.js b/chrome/renderer/resources/extensions/notification_custom_bindings.js new file mode 100644 index 0000000..b88be09 --- /dev/null +++ b/chrome/renderer/resources/extensions/notification_custom_bindings.js @@ -0,0 +1,128 @@ +// Copyright 2013 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. + +// Custom bindings for the notification API. + +var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); +var sendRequest = require('sendRequest').sendRequest; +var imageUtil = require('imageUtil'); +var lastError = require('lastError'); + +function url_getter(context, key) { + var f = function() { + return this[key]; + }; + return f.bind(context); +} + +function url_setter(context, key) { + var f = function(val) { + this[key] = val; + }; + return f.bind(context); +} + +function replaceNotificationOptionURLs(notification_details, callback) { + // A URL Spec is an object with the following keys: + // path: The resource to be downloaded. + // callback: A function to be called when the URL is complete. It + // should accept an ImageData object and set the appropriate + // field in the output of create. + + // TODO(dewittj): Try to remove hard-coding. + // |iconUrl| is required. + var url_specs = [{ + path: notification_details.iconUrl, + width: 80, + height: 80, + callback: url_setter(notification_details, 'iconUrl') + }]; + + // |secondIconUrl| is optional. + if (notification_details.secondIconUrl) { + url_specs.push({ + path: notification_details.secondIconUrl, + width: 80, + height: 80, + callback: url_setter(notification_details, 'secondIconUrl') + }); + } + + // |imageUrl| is optional. + if (notification_details.imageUrl) { + url_specs.push({ + path: notification_details.imageUrl, + width: 300, + height: 300, + callback: url_setter(notification_details, 'imageUrl') + }); + } + + // Each button has an optional icon. + var button_list = notification_details.buttons; + if (button_list && typeof button_list.length === 'number') { + var num_buttons = button_list.length; + for (var i = 0; i < num_buttons; i++) { + if (button_list[i].iconUrl) { + url_specs.push({ + path: button_list[i].iconUrl, + width: 16, + height: 16, + callback: url_setter(button_list[i], 'iconUrl') + }); + } + } + } + + var errors = 0; + + imageUtil.loadAllImages(url_specs, { + onerror: function(index) { + errors++; + }, + oncomplete: function(imageData) { + if (errors > 0) { + callback(false); + return; + } + for (var index = 0; index < url_specs.length; index++) { + var url_spec = url_specs[index]; + url_spec.callback(imageData[index]); + } + callback(true); + } + }); +} + +function genHandle(failure_function) { + return function(id, input_notification_details, callback) { + // TODO(dewittj): Remove this hack. This is used as a way to deep + // copy a complex JSON object. + var notification_details = JSON.parse( + JSON.stringify(input_notification_details)); + var that = this; + replaceNotificationOptionURLs(notification_details, function(success) { + if (success) { + sendRequest(that.name, + [id, notification_details, callback], + that.definition.parameters); + return; + } + lastError.set('Unable to download all specified images.'); + failure_function(callback, id); + }); + }; +} + +var handleCreate = genHandle(function(callback, id) { callback(id); }); +var handleUpdate = genHandle(function(callback, id) { callback(false); }); + +var experimentalNotificationCustomHook = function(bindingsAPI, extensionId) { + var apiFunctions = bindingsAPI.apiFunctions; + apiFunctions.setHandleRequest('create', handleCreate); + apiFunctions.setHandleRequest('update', handleCreate); +}; + +chromeHidden.registerCustomHook('experimental.notification', + experimentalNotificationCustomHook); diff --git a/chrome/renderer/resources/renderer_resources.grd b/chrome/renderer/resources/renderer_resources.grd index 1643ff4..225a75d 100644 --- a/chrome/renderer/resources/renderer_resources.grd +++ b/chrome/renderer/resources/renderer_resources.grd @@ -37,6 +37,7 @@ without changes to the corresponding grd file. fb9 --> <!-- Libraries. --> <include name="IDR_TAG_WATCHER_JS" file="extensions\tag_watcher.js" type="BINDATA" /> + <include name="IDR_IMAGE_UTIL_JS" file="extensions\image_util.js" type="BINDATA" /> <include name="IDR_LAST_ERROR_JS" file="extensions\last_error.js" type="BINDATA" /> <include name="IDR_SCHEMA_UTILS_JS" file="extensions\schema_utils.js" type="BINDATA" /> <include name="IDR_SEND_REQUEST_JS" file="extensions\send_request.js" type="BINDATA" /> @@ -65,6 +66,7 @@ without changes to the corresponding grd file. fb9 --> <include name="IDR_I18N_CUSTOM_BINDINGS_JS" file="extensions\i18n_custom_bindings.js" type="BINDATA" /> <include name="IDR_INPUT_IME_CUSTOM_BINDINGS_JS" file="extensions\input.ime_custom_bindings.js" type="BINDATA" /> <include name="IDR_MEDIA_GALLERIES_CUSTOM_BINDINGS_JS" file="extensions\media_galleries_custom_bindings.js" type="BINDATA" /> + <include name="IDR_NOTIFICATION_CUSTOM_BINDINGS_JS" file="extensions\notification_custom_bindings.js" type="BINDATA" /> <include name="IDR_OMNIBOX_CUSTOM_BINDINGS_JS" file="extensions\omnibox_custom_bindings.js" type="BINDATA" /> <include name="IDR_PAGE_ACTIONS_CUSTOM_BINDINGS_JS" file="extensions\page_actions_custom_bindings.js" type="BINDATA" /> <include name="IDR_PAGE_ACTION_CUSTOM_BINDINGS_JS" file="extensions\page_action_custom_bindings.js" type="BINDATA" /> diff --git a/chrome/test/data/extensions/api_test/notification/api/csp/background.js b/chrome/test/data/extensions/api_test/notification/api/csp/background.js new file mode 100644 index 0000000..266de2d --- /dev/null +++ b/chrome/test/data/extensions/api_test/notification/api/csp/background.js @@ -0,0 +1,165 @@ +// Copyright (c) 2012 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. + +const notification = chrome.experimental.notification; + +var idString = "foo"; + +var testCSP = function() { + var onCreateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.succeed(); + return; + } + chrome.test.fail(); + } + + var options = { + templateType: "basic", + iconUrl: "http://google.com/clearly-a-security-problem.png", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.create(idString, options, onCreateCallback); +}; + +function testDataURL() { + chrome.runtime.lastError = undefined; + var onCreateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.fail(); + return; + } + chrome.test.succeed(); + } + var options = { + templateType: "basic", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.create(idString, options, onCreateCallback); +} + +function testCSPUpdateIconURL() { + var onUpdateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.succeed(); + return; + } + chrome.test.fail(); + }; + var onCreateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.fail(); + return; + } + var options2 = { + templateType: "basic", + iconUrl: "http://www.google.com/favicon.ico", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.update(idString, options2, onUpdateCallback); + } + var options = { + templateType: "basic", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.create(idString, options, onCreateCallback); +} + +function testCSPUpdateImageURL() { + var onUpdateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.succeed(); + return; + } + chrome.test.fail(); + }; + var onCreateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.fail(); + return; + } + var options2 = { + templateType: "basic", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + imageUrl: "http://www.google.com/favicon.ico", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.update(idString, options2, onUpdateCallback); + } + var options = { + templateType: "image", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + notification.create(idString, options, onCreateCallback); +} + +function testCSPUpdateButtonIconURL() { + var onUpdateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.succeed(); + return; + } + chrome.test.fail(); + }; + var onCreateCallback = function(id) { + if (chrome.runtime.lastError) { + chrome.test.fail(); + return; + } + var options2 = { + templateType: "basic", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + title: "Attention!", + buttons: [ { + title: "Foo", + iconUrl: "http://www.google.com/favicon.ico" + } ], + message: "Check out Cirque du Soleil" + }; + notification.update(idString, options2, onUpdateCallback); + } + var options = { + templateType: "basic", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==", + title: "Attention!", + buttons: [ { + title: "Foo", + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA" + + "CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHw" + + "AAAABJRU5ErkJggg==" + } ], + message: "Check out Cirque du Soleil" + }; + notification.create(idString, options, onCreateCallback); +} +chrome.test.runTests([ + testCSP, + testDataURL, + testCSPUpdateIconURL, + testCSPUpdateImageURL, + testCSPUpdateButtonIconURL ]); diff --git a/chrome/test/data/extensions/api_test/notification/api/csp/manifest.json b/chrome/test/data/extensions/api_test/notification/api/csp/manifest.json new file mode 100644 index 0000000..de79f22 --- /dev/null +++ b/chrome/test/data/extensions/api_test/notification/api/csp/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "chrome.notification", + "version": "0.1", + "description": "chrome.notification API events", + "app": { + "background": { + "scripts": ["background.js"] + } + }, + "permissions": [ + "experimental" + ] +} diff --git a/chrome/test/data/extensions/api_test/notification/api/events/background.js b/chrome/test/data/extensions/api_test/notification/api/events/background.js index f041b53..7aa21ec 100644 --- a/chrome/test/data/extensions/api_test/notification/api/events/background.js +++ b/chrome/test/data/extensions/api_test/notification/api/events/background.js @@ -26,8 +26,7 @@ var testBasicEvents = function() { var options = { templateType: "basic", - iconUrl: "http://www.google.com/intl/en/chrome/assets/" + - "common/images/chrome_logo_2x.png", + iconUrl: "/icon.png", title: "Attention!", message: "Check out Cirque du Soleil" }; |