Bug 871350 - Uplift Add-on SDK integration branch to Firefox

This commit is contained in:
Wes Kocher 2013-05-12 19:18:37 -07:00
parent ca42cc8c25
commit b88d9e9bfb
16 changed files with 388 additions and 49 deletions

View File

@ -10,6 +10,9 @@ content of web pages or be notified when the user clicks a link.
The SDK provides several core modules to support this:
**[context-menu](modules/sdk/context-menu.html)**<br>
Add items to the browser's context menu.
**[panel](modules/sdk/panel.html)**<br>
Create a dialog that can host web content.
@ -19,12 +22,12 @@ Retrieve a page and access its content, without displaying it to the user.
**[page-mod](modules/sdk/page-mod.html)**<br>
Execute scripts in the context of selected web pages.
**[tabs](modules/sdk/tabs.html)**<br>
Manipulate the browser's tabs, including the web content displayed in the tab.
**[widget](modules/sdk/widget.html)**<br>
Host an add-on's user interface, including web content.
**[context-menu](modules/sdk/context-menu.html)**<br>
Add items to the browser's context menu.
Firefox is moving towards a model in which it uses separate
processes to display the UI, handle web content, and execute add-ons. The main
add-on code will run in the add-on process and will not have direct access to

View File

@ -79,7 +79,8 @@ user-defined messages.
### Accessing `port` in the Add-on Script ###
In the add-on code, the channel of communication between the add-on and a
particular content script context is encapsulated by the `worker` object. Thus
particular content script context is encapsulated by the
[`worker`](modules/sdk/content/worker.html#Worker) object. Thus
the `port` object for communicating with a content script is a property of the
corresponding `worker` object.
@ -117,7 +118,8 @@ handle multiple pages, each with its own context in which the content scripts
are executing, so it needs a separate channel (worker) for each page.
So `page-mod` does not integrate the worker API directly: instead, each time a
content script is attached to a page, the worker associated with the page is
content script is attached to a page, the
[worker](modules/sdk/content/worker.html#Worker) associated with the page is
supplied to the page-mod in its `onAttach` function. By supplying a target for
this function in the page-mod's constructor you can register to receive
messages from the content script, and take a reference to the worker so as to

View File

@ -39,7 +39,7 @@ function. Again, `panel` and `page` integrate `worker` directly:
panel.postMessage(addonMessage);
However, for `page-mod` objects you need to listen to the `onAttach` event
and use the worker supplied to that:
and use the [worker](modules/sdk/content/worker.html#Worker) supplied to that:
var pageMod = require('sdk/page-mod').PageMod({
include: ['*'],

View File

@ -99,7 +99,8 @@ message from one side to the other, the sender calls `port.emit()` and
the receiver listens using `port.on()`.
* In the content script, `port` is a property of the global `self` object.
* In the add-on script, `tab-attach()` returns an object containing the
* In the add-on script, `tab.attach()` returns a
[worker](modules/sdk/content/worker.html#Worker) object containing the
`port` property you use to send messages to the content script.
Let's rewrite the example above to pass a message from the add-on to

View File

@ -146,7 +146,8 @@ the receiver listens using `port.on()`.
* In the content script, `port` is a property of the global `self` object.
* In the add-on script, you need to listen for the `onAttach` event to get
passed an object that contains `port`.
passed a [worker](modules/sdk/content/worker.html#Worker) object that
contains `port`.
Let's rewrite the example above to pass a message from the add-on to
the content script. The message will contain the new content to insert into

View File

@ -7,8 +7,8 @@
This module is used in the internal implementation of SDK modules
which use
[content scripts to interact with web content](dev-guide/guides/content-scripts/index.html),
such as the [`panel`](modules/sdk/panel.html) or [`page-mod`](modules/sdk/page-mod.html)
modules.
such as the [`tabs`](modules/sdk/tabs.html), [`panel`](modules/sdk/panel.html),
or [`page-mod`](modules/sdk/page-mod.html) modules.
It exports the `Worker` trait, which enables content
scripts and the add-on code to exchange messages using the
@ -121,7 +121,7 @@ error that occurs in one of the content scripts.
@argument {Error}
The event listener is passed a single argument which is an
[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error)
[Error](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error)
object.
</api>
@ -129,6 +129,19 @@ object.
@event
This event is emitted when the document associated with this worker is unloaded
or the worker's `destroy()` method is called.
Note that you can't communicate with the content script in response to this
event. If you try, you'll see this error:
<pre>Error: Couldn't find the worker to receive this message.
The script may not be initialized yet, or may already have been unloaded</pre>
You can handle the `detach` event in the content script itself though:
// in content script
self.on("detach", function() {
window.close();
});
</api>
</api>

View File

@ -0,0 +1,48 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
The `places/favicon` module provides simple helper functions for working with favicons.
<api name="getFavicon">
@function
Takes an `object` that represents a page's URL and returns a promise
that resolves with the favicon URL for that page. The `object` can
be a URL `String` or a [`Tab`](modules/sdk/tabs.html#Tab). The platform service
([mozIAsyncFavicons](https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/mozIAsyncFavicons)) retrieves favicon data stored from previously visited sites, and as
such, will only return favicon URLs for visited sites.
let { getFavicon } = require("sdk/places/favicon");
// String example
getFavicon("http://mozilla.org").then(function (url) {
console.log(url); // http://mozorg.cdn.mozilla.net/media/img/favicon.ico
});
// Tab example
require("sdk/tabs").open({
url: "http://mozilla.org",
onReady: function (tab) {
getFavicon(tab).then(function (url) {
console.log(url); // http://mozorg.cdn.mozilla.net/media/img/favicon.ico
});
}
});
// An optional callback can be provided to handle
// the promise's resolve and reject states
getFavicon("http://mozilla.org", function (url) {
console.log(url); // http://mozorg.cdn.mozilla.net/media/img/favicon.ico
});
@param object {string, tab}
A value that represents the URL of the page to get the favicon URL from.
Can be a URL `String` or a [`Tab`](modules/sdk/tabs.html#Tab).
@param callback {function}
An optional callback function that will be used in both resolve and
reject cases.
@returns {promise}
A promise that resolves with the favicon URL.
</api>

View File

@ -55,7 +55,8 @@ property you can load a new page in the tab:
You can attach a [content script](dev-guide/guides/content-scripts/index.html)
to the page hosted in a tab, and use that to access and manipulate the page's
content:
content (see the
[Modifying the Page Hosted by a Tab](dev-guide/tutorials/modifying-web-pages-tab.html) tutorial):
var tabs = require("sdk/tabs");
@ -213,6 +214,12 @@ This property can be set to load a different URL in the tab.
@property {string}
The URL of the favicon for the page currently loaded in the tab.
This property is read-only.
<div class="warning">
This property is deprecated.
From version 1.15, use the <a href="modules/sdk/places/favicon.html#getFavicon()">favicon module's <code>getFavicon()</code></a> function instead.
</div>
</api>
<api name="contentType">
@ -312,8 +319,10 @@ Returns thumbnail data URI of the page currently loaded in this tab.
content script. Optional.
@returns {Worker}
The [Worker](modules/sdk/content/worker.html#Worker) object can be used to
communicate with the content script.
See [Content Scripts guide](dev-guide/guides/content-scripts/index.html)
to learn how to use the `Worker` object to communicate with the content script.
to learn the details.
</api>

View File

@ -10,9 +10,6 @@ module.metadata = {
const { Cc, Ci, Cu } = require("chrome");
const base64 = require("../base64");
const { defer } = require("../core/promise");
const { newURI } = require("../url/utils");
const IOService = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
@ -20,33 +17,11 @@ const { deprecateFunction } = require('../util/deprecate');
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm");
const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
getService(Ci.nsIFaviconService);
const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons);
const PNG_B64 = "data:image/png;base64,";
const DEF_FAVICON_URI = "chrome://mozapps/skin/places/defaultFavicon.png";
let DEF_FAVICON = null;
/**
* Takes URI of the page and returns a promise that resolves
* to the page's favicon URI.
* @param {String} uri
* @param {Function} (callback)
* @returns {Promise}
*/
exports.getFavicon = function getFavicon(uri, callback) {
let pageURI = newURI(uri);
let deferred = defer();
AsyncFavicons.getFaviconURLForPage(pageURI, function (aURI) {
if (aURI && aURI.spec)
deferred.resolve(aURI.spec.toString());
else
deferred.reject(null);
});
if (callback) deferred.promise.then(callback, callback);
return deferred.promise;
};
/**
* Takes URI of the page and returns associated favicon URI.
* If page under passed uri has no favicon then base64 encoded data URI of

View File

@ -0,0 +1,45 @@
/* 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";
module.metadata = {
"stability": "unstable"
};
const { Cc, Ci, Cu } = require("chrome");
const { defer, reject } = require("../core/promise");
const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
getService(Ci.nsIFaviconService);
const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons);
const { isValidURI } = require("../url");
const { newURI, getURL } = require("../url/utils");
/**
* Takes an object of several possible types and
* returns a promise that resolves to the page's favicon URI.
* @param {String|Tab} object
* @param {Function} (callback)
* @returns {Promise}
*/
function getFavicon (object, callback) {
let url = getURL(object);
let deferred = defer();
if (url && isValidURI(url)) {
AsyncFavicons.getFaviconURLForPage(newURI(url), function (aURI) {
if (aURI && aURI.spec)
deferred.resolve(aURI.spec.toString());
else
deferred.reject(null);
});
} else {
deferred.reject(null);
}
if (callback) deferred.promise.then(callback, callback);
return deferred.promise;
}
exports.getFavicon = getFavicon;

View File

@ -1,4 +1,4 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*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';
@ -14,6 +14,8 @@ const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle,
getTabURL, setTabURL, getTabContentType, getTabId } = require('./utils');
const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils');
const viewNS = require('sdk/core/namespace').ns();
const { deprecateUsage } = require('sdk/util/deprecate');
const { getURL } = require('sdk/url/utils');
// Array of the inner instances of all the wrapped tabs.
const TABS = [];
@ -63,6 +65,9 @@ const TabTrait = Trait.compose(EventEmitter, {
viewNS(this._public).tab = this._tab;
getPBOwnerWindow.implement(this._public, getChromeTab);
// Add tabs to getURL method
getURL.implement(this._public, (function (obj) this._public.url).bind(this));
// Since we will have to identify tabs by a DOM elements facade function
// is used as constructor that collects all the instances and makes sure
// that they more then one wrapper is not created per tab.
@ -171,7 +176,13 @@ const TabTrait = Trait.compose(EventEmitter, {
* URI of the favicon for the page currently loaded in this tab.
* @type {String}
*/
get favicon() this._tab ? getFaviconURIForLocation(this.url) : undefined,
get favicon() {
deprecateUsage(
'tab.favicon is deprecated, ' +
'please use require("sdk/places/favicon").getFavicon instead.'
);
return this._tab ? getFaviconURIForLocation(this.url) : undefined
},
/**
* The CSS style for the tab
*/

View File

@ -12,10 +12,18 @@ const { Cc, Ci, Cr } = require("chrome");
const IOService = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
const { isValidURI } = require("../url");
const { method } = require("../../method/core");
function newURI (uri) {
if (!isValidURI(uri))
throw new Error("malformed URI: " + uri);
return IOService.newURI(uri, null, null);
}
exports.newURI = newURI;
let getURL = method('sdk/url:getURL');
getURL.define(String, function (url) url);
getURL.define(function (object) {
return null;
});
exports.getURL = getURL;

View File

@ -73,11 +73,10 @@ exports.remove = function remove(array, element) {
* @returns {Array}
*/
exports.unique = function unique(array) {
var value = [];
return array.forEach(function(element) {
add(value, element);
});
return value;
return array.reduce(function(values, element) {
add(values, element);
return values;
}, []);
};
exports.flatten = function flatten(array){

View File

@ -171,7 +171,7 @@ exports.testAutomaticDestroy = function(test) {
exports.testTabProperties = function(test) {
test.waitUntilDone();
openBrowserWindow(function(window, browser) {
let tabs= require("sdk/tabs");
let tabs = require('sdk/tabs');
let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
tabs.open({
url: url,
@ -923,7 +923,7 @@ exports['test unique tab ids'] = function(test) {
win: win
});
});
return deferred.promise;
}
@ -934,7 +934,7 @@ exports['test unique tab ids'] = function(test) {
results[0].win.close();
results[1].win.close();
test.done();
});
});
}
// related to Bug 671305
@ -1028,6 +1028,30 @@ exports.testOnPageShowEvent = function (test) {
});
};
exports.testFaviconGetterDeprecation = function (test) {
const { LoaderWithHookedConsole } = require("sdk/test/loader");
let { loader, messages } = LoaderWithHookedConsole(module);
let tabs = loader.require('sdk/tabs');
test.waitUntilDone();
tabs.open({
url: 'data:text/html;charset=utf-8,',
onOpen: function (tab) {
let favicon = tab.favicon;
test.assert(messages.length === 1, 'only one error is dispatched');
test.assert(messages[0].type, 'error', 'the console message is an error');
let msg = messages[0].msg;
test.assert(msg.indexOf('tab.favicon is deprecated') !== -1,
'message contains the given message');
tab.close(test.done.bind(test));
loader.unload();
}
});
}
/******************* helpers *********************/
// Helper for getting the active window

View File

@ -64,3 +64,24 @@ exports.testFlatten = function(test) {
test.assertEqual(array.flatten([1, [2, [3]]]).length, 3);
test.assertEqual(array.flatten([[1], [[2, [3]]]]).length, 3);
};
exports.testUnique = function(test) {
var Class = function () {};
var A = {};
var B = new Class();
var C = [ 1, 2, 3 ];
var D = {};
var E = new Class();
compareArray(array.unique([1,2,3,1,2]), [1,2,3]);
compareArray(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
compareArray(array.unique([A, A, A, B, B, D]), [A,B,D]);
compareArray(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
function compareArray (a, b) {
test.assertEqual(a.length, b.length);
for (let i = 0; i < a.length; i++) {
test.assertEqual(a[i], b[i]);
}
}
};

View File

@ -0,0 +1,179 @@
/* 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/. */
const { Cc, Ci, Cu } = require('chrome');
const { getFavicon } = require('sdk/places/favicon');
const tabs = require('sdk/tabs');
const open = tabs.open;
const port = 8099;
const host = 'http://localhost:' + port;
const { onFaviconChange, serve, binFavicon } = require('./favicon-helpers');
const { once } = require('sdk/system/events');
const { defer } = require('sdk/core/promise');
const faviconService = Cc["@mozilla.org/browser/favicon-service;1"].
getService(Ci.nsIFaviconService);
exports.testStringGetFaviconCallbackSuccess = function (assert, done) {
let name = 'callbacksuccess'
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let favicon = host + '/' + name + '.ico';
let tab;
onFaviconChange(url, function (faviconUrl) {
getFavicon(url, function (url) {
assert.equal(favicon, url, 'Callback returns correct favicon url');
complete(tab, srv, done);
});
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testStringGetFaviconCallbackFailure = function (assert, done) {
let name = 'callbackfailure';
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let tab;
waitAndExpire(url).then(function () {
getFavicon(url, function (url) {
assert.equal(url, null, 'Callback returns null');
complete(tab, srv, done);
});
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testStringGetFaviconPromiseSuccess = function (assert, done) {
let name = 'promisesuccess'
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let favicon = host + '/' + name + '.ico';
let tab;
onFaviconChange(url, function (faviconUrl) {
getFavicon(url).then(function (url) {
assert.equal(url, favicon, 'Callback returns null');
}, function (err) {
assert.fail('Reject should not be called');
}).then(complete.bind(null, tab, srv, done));
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testStringGetFaviconPromiseFailure = function (assert, done) {
let name = 'promisefailure'
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let tab;
waitAndExpire(url).then(function () {
getFavicon(url).then(invalidResolve(assert), validReject(assert, 'expired url'))
.then(complete.bind(null, tab, srv, done));
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testTabsGetFaviconPromiseSuccess = function (assert, done) {
let name = 'tabs-success'
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let favicon = host + '/' + name + '.ico';
let tab;
onFaviconChange(url, function () {
getFavicon(tab).then(function (url) {
assert.equal(url, favicon, "getFavicon should return url for tab");
complete(tab, srv, done);
});
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testTabsGetFaviconPromiseFailure = function (assert, done) {
let name = 'tabs-failure'
let srv = makeServer(name);
let url = host + '/' + name + '.html';
let tab;
waitAndExpire(url).then(function () {
getFavicon(tab).then(invalidResolve(assert), validReject(assert, 'expired tab'))
.then(complete.bind(null, tab, srv, done));
});
open({
url: url,
onOpen: function (newTab) tab = newTab,
inBackground: true
});
};
exports.testRejects = function (assert, done) {
getFavicon({})
.then(invalidResolve(assert), validReject(assert, 'Object'))
.then(getFavicon(null))
.then(invalidResolve(assert), validReject(assert, 'null'))
.then(getFavicon(undefined))
.then(invalidResolve(assert), validReject(assert, 'undefined'))
.then(getFavicon([]))
.then(invalidResolve(assert), validReject(assert, 'Array'))
.then(done);
};
function invalidResolve (assert) {
return function () assert.fail('Promise should not be resolved successfully');
}
function validReject (assert, name) {
return function () assert.pass(name + ' correctly rejected');
}
function makeServer (name) {
return serve({name: name, favicon: binFavicon, port: port, host: host});
}
function waitAndExpire (url) {
let deferred = defer();
onFaviconChange(url, function (faviconUrl) {
once('places-favicons-expired', function () {
deferred.resolve();
});
faviconService.expireAllFavicons();
});
return deferred.promise;
}
function complete(tab, srv, done) {
tab.close(function () {
srv.stop(done);
})
}
require("test").run(exports);