Merge mozilla-central to b2g-inbound

This commit is contained in:
Carsten "Tomcat" Book 2015-08-04 13:58:28 +02:00
Родитель d2e64f7264 57a966656a
Коммит 3531b883ec
512 изменённых файлов: 12638 добавлений и 12064 удалений

Просмотреть файл

@ -22,4 +22,4 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more.
Bug 1190180 - need clobber for backouts
Bug 1186748 needed a CLOBBER again

Просмотреть файл

@ -692,7 +692,8 @@ void
DocAccessible::AttributeWillChange(nsIDocument* aDocument,
dom::Element* aElement,
int32_t aNameSpaceID,
nsIAtom* aAttribute, int32_t aModType)
nsIAtom* aAttribute, int32_t aModType,
const nsAttrValue* aNewValue)
{
Accessible* accessible = GetAccessible(aElement);
if (!accessible) {
@ -733,7 +734,8 @@ void
DocAccessible::AttributeChanged(nsIDocument* aDocument,
dom::Element* aElement,
int32_t aNameSpaceID, nsIAtom* aAttribute,
int32_t aModType)
int32_t aModType,
const nsAttrValue* aOldValue)
{
NS_ASSERTION(!IsDefunct(),
"Attribute changed called on defunct document accessible!");

Просмотреть файл

@ -11,7 +11,6 @@ const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar',
'menu_readingListSidebar'
];
function isSidebarShowing(window) {

Просмотреть файл

@ -17,7 +17,6 @@ const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar',
'menu_readingListSidebar'
];
function isSidebarShowing(window) {

Просмотреть файл

@ -1927,11 +1927,6 @@ pref("dom.ipc.reportProcessHangs", false);
pref("dom.ipc.reportProcessHangs", true);
#endif
pref("browser.readinglist.enabled", false);
pref("browser.readinglist.sidebarEverOpened", false);
pref("readinglist.scheduler.enabled", false);
pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
pref("browser.reader.detectedFirstArticle", false);
// Don't limit how many nodes we care about on desktop:
pref("reader.parse-node-limit", 0);

Просмотреть файл

@ -49,6 +49,7 @@ const gXPInstallObserver = {
var options = {
displayURI: installInfo.originatingURI,
timeout: Date.now() + 30000,
removeOnDismissal: true,
};
let cancelInstallation = () => {

Просмотреть файл

@ -210,10 +210,6 @@
key="key_gotoHistory"
observes="viewHistorySidebar"
label="&historyButton.label;"/>
<menuitem id="menu_readingListSidebar"
key="key_readingListSidebar"
observes="readingListSidebar"
label="&readingList.label;"/>
<!-- Service providers with sidebars are inserted between these two menuseperators -->
<menuseparator hidden="true"/>
@ -443,30 +439,6 @@
onpopupshowing="if (!this.parentNode._placesView)
new PlacesMenu(event, 'place:folder=TOOLBAR');"/>
</menu>
#ifndef XP_MACOSX
# Disabled on Mac because we can't fill native menupopups asynchronously
<menuseparator id="menu_readingListSeparator">
<observes element="readingListSidebar" attribute="hidden"/>
</menuseparator>
<menu id="menu_readingList"
class="menu-iconic bookmark-item"
label="&readingList.label;"
container="true">
<observes element="readingListSidebar" attribute="hidden"/>
<menupopup id="readingListPopup"
#ifndef XP_MACOSX
placespopup="true"
#endif
onpopupshowing="ReadingListUI.onReadingListPopupShowing(this);">
<menuseparator id="viewReadingListSidebarSeparator"/>
<menuitem id="viewReadingListSidebar" class="subviewbutton"
oncommand="SidebarUI.toggle('readingListSidebar');"
label="&readingList.showSidebar.label;">
<observes element="readingListSidebar" attribute="checked"/>
</menuitem>
</menupopup>
</menu>
#endif
<menuseparator id="bookmarksMenuItemsSeparator"/>
<!-- Bookmarks menu items -->
<menuseparator builder="end"

Просмотреть файл

@ -1,376 +0,0 @@
/*
# 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/.
*/
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
const READINGLIST_COMMAND_ID = "readingListSidebar";
let ReadingListUI = {
/**
* Frame-script messages we want to listen to.
* @type {[string]}
*/
MESSAGES: [
"ReadingList:GetVisibility",
"ReadingList:ToggleVisibility",
"ReadingList:ShowIntro",
],
/**
* Add-to-ReadingList toolbar button in the URLbar.
* @type {Element}
*/
toolbarButton: null,
/**
* Whether this object is currently registered as a listener with ReadingList.
* Used to avoid inadvertantly loading the ReadLingList.jsm module on startup.
* @type {Boolean}
*/
listenerRegistered: false,
/**
* Initialize the ReadingList UI.
*/
init() {
this.toolbarButton = document.getElementById("readinglist-addremove-button");
Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
const mm = window.messageManager;
for (let msg of this.MESSAGES) {
mm.addMessageListener(msg, this);
}
this.updateUI();
},
/**
* Un-initialize the ReadingList UI.
*/
uninit() {
Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
const mm = window.messageManager;
for (let msg of this.MESSAGES) {
mm.removeMessageListener(msg, this);
}
if (this.listenerRegistered) {
ReadingList.removeListener(this);
this.listenerRegistered = false;
}
},
/**
* Whether the ReadingList feature is enabled or not.
* @type {boolean}
*/
get enabled() {
return Preferences.get("browser.readinglist.enabled", false);
},
/**
* Whether the ReadingList sidebar is currently open or not.
* @type {boolean}
*/
get isSidebarOpen() {
return SidebarUI.isOpen && SidebarUI.currentID == READINGLIST_COMMAND_ID;
},
/**
* Update the UI status, ensuring the UI is shown or hidden depending on
* whether the feature is enabled or not.
*/
updateUI() {
let enabled = this.enabled;
if (enabled) {
// This is a no-op if we're already registered.
ReadingList.addListener(this);
this.listenerRegistered = true;
} else {
if (this.listenerRegistered) {
// This is safe to call if we're not currently registered, but we don't
// want to forcibly load the normally lazy-loaded module on startup.
ReadingList.removeListener(this);
this.listenerRegistered = false;
}
this.hideSidebar();
}
document.getElementById(READINGLIST_COMMAND_ID).setAttribute("hidden", !enabled);
document.getElementById(READINGLIST_COMMAND_ID).setAttribute("disabled", !enabled);
},
/**
* Show the ReadingList sidebar.
* @return {Promise}
*/
showSidebar() {
if (this.enabled) {
return SidebarUI.show(READINGLIST_COMMAND_ID);
}
},
/**
* Hide the ReadingList sidebar, if it is currently shown.
*/
hideSidebar() {
if (this.isSidebarOpen) {
SidebarUI.hide();
}
},
/**
* Re-refresh the ReadingList bookmarks submenu when it opens.
*
* @param {Element} target - Menu element opening.
*/
onReadingListPopupShowing: Task.async(function* (target) {
if (target.id == "BMB_readingListPopup") {
// Setting this class in the .xul file messes with the way
// browser-places.js inserts bookmarks in the menu.
document.getElementById("BMB_viewReadingListSidebar")
.classList.add("panel-subview-footer");
}
while (!target.firstChild.id)
target.firstChild.remove();
let classList = "menuitem-iconic bookmark-item menuitem-with-favicon";
let insertPoint = target.firstChild;
if (insertPoint.classList.contains("subviewbutton"))
classList += " subviewbutton";
let hasItems = false;
yield ReadingList.forEachItem(item => {
hasItems = true;
let menuitem = document.createElement("menuitem");
menuitem.setAttribute("label", item.title || item.url);
menuitem.setAttribute("class", classList);
let node = menuitem._placesNode = {
// Passing the PlacesUtils.nodeIsURI check is required for the
// onCommand handler to load our URI.
type: Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
// makes PlacesUIUtils.canUserRemove return false.
// The context menu is broken without this.
parent: {type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER},
// A -1 id makes this item a non-bookmark, which avoids calling
// PlacesUtils.annotations.itemHasAnnotation to check if the
// bookmark should be opened in the sidebar (this call fails for
// readinglist item, and breaks loading our URI).
itemId: -1,
// Used by the tooltip and onCommand handlers.
uri: item.url,
// Used by the tooltip.
title: item.title
};
Favicons.getFaviconURLForPage(item.uri, uri => {
if (uri) {
menuitem.setAttribute("image",
Favicons.getFaviconLinkForIcon(uri).spec);
}
});
target.insertBefore(menuitem, insertPoint);
}, {sort: "addedOn", descending: true});
if (!hasItems) {
let menuitem = document.createElement("menuitem");
let bundle =
Services.strings.createBundle("chrome://browser/locale/places/places.properties");
menuitem.setAttribute("label", bundle.GetStringFromName("bookmarksMenuEmptyFolder"));
menuitem.setAttribute("class", "bookmark-item");
menuitem.setAttribute("disabled", true);
target.insertBefore(menuitem, insertPoint);
}
}),
/**
* Hide the ReadingList sidebar, if it is currently shown.
*/
toggleSidebar() {
if (this.enabled) {
SidebarUI.toggle(READINGLIST_COMMAND_ID);
}
},
/**
* Respond to messages.
*/
receiveMessage(message) {
switch (message.name) {
case "ReadingList:GetVisibility": {
if (message.target.messageManager) {
message.target.messageManager.sendAsyncMessage("ReadingList:VisibilityStatus",
{ isOpen: this.isSidebarOpen });
}
break;
}
case "ReadingList:ToggleVisibility": {
this.toggleSidebar();
break;
}
case "ReadingList:ShowIntro": {
if (this.enabled && !Preferences.get("browser.readinglist.introShown", false)) {
Preferences.set("browser.readinglist.introShown", true);
this.showSidebar();
}
break;
}
}
},
/**
* Handles toolbar button styling based on page proxy state changes.
*
* @see SetPageProxyState()
*
* @param {string} state - New state. Either "valid" or "invalid".
*/
onPageProxyStateChanged: Task.async(function* (state) {
if (!this.toolbarButton) {
// nothing to do if we have no button.
return;
}
let uri;
if (this.enabled && state == "valid") {
uri = gBrowser.currentURI;
if (uri.schemeIs("about"))
uri = ReaderMode.getOriginalUrl(uri.spec);
else if (!uri.schemeIs("http") && !uri.schemeIs("https"))
uri = null;
}
let msg = {topic: "UpdateActiveItem", url: null};
if (!uri) {
this.toolbarButton.setAttribute("hidden", true);
if (this.isSidebarOpen)
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
return;
}
let isInList = yield ReadingList.hasItemForURL(uri);
if (window.closed) {
// Skip updating the UI if the window was closed since our hasItemForURL call.
return;
}
if (this.isSidebarOpen) {
if (isInList)
msg.url = typeof uri == "string" ? uri : uri.spec;
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
}
this.setToolbarButtonState(isInList);
}),
/**
* Set the state of the ReadingList toolbar button in the urlbar.
* If the current tab's page is in the ReadingList (active), sets the button
* to allow removing the page. Otherwise, sets the button to allow adding the
* page (not active).
*
* @param {boolean} active - True if the button should be active (page is
* already in the list).
*/
setToolbarButtonState(active) {
this.toolbarButton.setAttribute("already-added", active);
let type = (active ? "remove" : "add");
let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`);
this.toolbarButton.setAttribute("tooltiptext", tooltip);
this.toolbarButton.removeAttribute("hidden");
},
buttonClick(event) {
if (event.button != 0) {
return;
}
this.togglePageByBrowser(gBrowser.selectedBrowser);
},
/**
* Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or
* removing otherwise.
*
* @param {<xul:browser>} browser - Browser with page to toggle.
* @returns {Promise} Promise resolved when operation has completed.
*/
togglePageByBrowser: Task.async(function* (browser) {
let uri = browser.currentURI;
if (uri.spec.startsWith("about:reader?"))
uri = ReaderMode.getOriginalUrl(uri.spec);
if (!uri)
return;
let item = yield ReadingList.itemForURL(uri);
if (item) {
yield item.delete();
} else {
yield ReadingList.addItemFromBrowser(browser, uri);
}
}),
/**
* Checks if a given item matches the current tab in this window.
*
* @param {ReadingListItem} item - Item to check
* @returns True if match, false otherwise.
*/
isItemForCurrentBrowser(item) {
let currentURL = gBrowser.currentURI.spec;
if (currentURL.startsWith("about:reader?"))
currentURL = ReaderMode.getOriginalUrl(currentURL);
if (item.url == currentURL || item.resolvedURL == currentURL) {
return true;
}
return false;
},
/**
* ReadingList event handler for when an item is added.
*
* @param {ReadingListItem} item - Item added.
*/
onItemAdded(item) {
if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) {
SidebarUI.show("readingListSidebar");
}
if (this.isItemForCurrentBrowser(item)) {
this.setToolbarButtonState(true);
if (this.isSidebarOpen) {
let msg = {topic: "UpdateActiveItem", url: item.url};
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
}
}
},
/**
* ReadingList event handler for when an item is deleted.
*
* @param {ReadingListItem} item - Item deleted.
*/
onItemDeleted(item) {
if (this.isItemForCurrentBrowser(item)) {
this.setToolbarButtonState(false);
}
},
};

Просмотреть файл

@ -146,11 +146,6 @@
sidebarurl="chrome://browser/content/history/history-panel.xul"
oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
<broadcaster id="readingListSidebar" hidden="true" autoCheck="false" disabled="true"
sidebartitle="&readingList.label;" type="checkbox" group="sidebar"
sidebarurl="chrome://browser/content/readinglist/sidebar.xhtml"
oncommand="SidebarUI.toggle('readingListSidebar');"/>
<broadcaster id="viewWebPanelsSidebar" autoCheck="false"
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/>
@ -421,11 +416,6 @@
#endif
command="viewHistorySidebar"/>
<key id="key_readingListSidebar"
key="&readingList.sidebar.commandKey;"
modifiers="accel,alt"
command="readingListSidebar"/>
<key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
<key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
<key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>

Просмотреть файл

@ -11,9 +11,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
let CloudSync = null;
#endif
XPCOMUtils.defineLazyModuleGetter(this, "ReadingListScheduler",
"resource:///modules/readinglist/Scheduler.jsm");
// gSyncUI handles updating the tools menu and displaying notifications.
let gSyncUI = {
_obs: ["weave:service:sync:start",
@ -31,10 +28,6 @@ let gSyncUI = {
"weave:ui:sync:error",
"weave:ui:sync:finish",
"weave:ui:clear-error",
"readinglist:sync:start",
"readinglist:sync:finish",
"readinglist:sync:error",
],
_unloaded: false,
@ -115,17 +108,13 @@ let gSyncUI = {
// authManager, so this should always return a value directly.
// This only applies to fxAccounts-based Sync.
if (Weave.Status._authManager._signedInUser !== undefined) {
// So we are using Firefox accounts - in this world, checking Sync isn't
// enough as reading list may be configured but not Sync.
// We consider ourselves setup if we have a verified user.
// XXX - later we should consider checking preferences to ensure at least
// one engine is enabled?
// If we have a signed in user already, and that user is not verified,
// revert to the "needs setup" state.
return !Weave.Status._authManager._signedInUser ||
!Weave.Status._authManager._signedInUser.verified;
}
// So we are using legacy sync, and reading-list isn't supported for such
// users, so check sync itself.
// We are using legacy sync - check that.
let firstSync = "";
try {
firstSync = Services.prefs.getCharPref("services.sync.firstSync");
@ -136,10 +125,9 @@ let gSyncUI = {
},
_loginFailed: function () {
this.log.debug("_loginFailed has sync state=${sync}, readinglist state=${rl}",
{ sync: Weave.Status.login, rl: ReadingListScheduler.state});
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED ||
ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
this.log.debug("_loginFailed has sync state=${sync}",
{ sync: Weave.Status.login});
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
},
updateUI: function SUI_updateUI() {
@ -235,8 +223,6 @@ let gSyncUI = {
onLoginError: function SUI_onLoginError() {
this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
// Note: This is used for *both* Sync and ReadingList login errors.
// if login fails, any other notifications are essentially moot
Weave.Notifications.removeAll();
// if we haven't set up the client, don't show errors
@ -260,12 +246,10 @@ let gSyncUI = {
},
showLoginError() {
// Note: This is used for *both* Sync and ReadingList login errors.
let title = this._stringBundle.GetStringFromName("error.login.title");
let description;
if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE ||
this.isProlongedReadingListError()) {
if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
this.log.debug("showLoginError has a prolonged login error");
// Convert to days
let lastSync =
@ -333,7 +317,6 @@ let gSyncUI = {
}
Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
Services.obs.notifyObservers(null, "readinglist:user-sync", null);
},
handleToolbarButton: function SUI_handleStatusbarButton() {
@ -432,14 +415,6 @@ let gSyncUI = {
lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
}
catch (e) { };
// and reading-list time - we want whatever one is the most recent.
try {
let lastRLSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
if (!lastSync || lastRLSync > lastSync) {
lastSync = lastRLSync;
}
}
catch (e) { };
if (!lastSync || this._needsSetup()) {
if (syncButton) {
syncButton.removeAttribute("tooltiptext");
@ -475,75 +450,6 @@ let gSyncUI = {
this.clearError(title);
},
// Return true if the reading-list is in a "prolonged" error state. That
// engine doesn't impose what that means, so calculate it here. For
// consistency, we just use the sync prefs.
isProlongedReadingListError() {
// If the readinglist scheduler is disabled we don't treat it as prolonged.
let enabled = false;
try {
enabled = Services.prefs.getBoolPref("readinglist.scheduler.enabled");
} catch (_) {}
if (!enabled) {
return false;
}
let lastSync, threshold, prolonged;
try {
lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
prolonged = lastSync <= threshold;
} catch (ex) {
// no pref, assume not prolonged.
prolonged = false;
}
this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
{lastSync, threshold, prolonged});
return prolonged;
},
onRLSyncError() {
// Like onSyncError, but from the reading-list engine.
// However, the current UX around Sync is that error notifications should
// generally *not* be seen as they typically aren't actionable - so only
// authentication errors (which require user action) and "prolonged" errors
// (which technically aren't actionable, but user really should know anyway)
// are shown.
this.log.debug("onRLSyncError with readingList state", ReadingListScheduler.state);
if (ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION) {
this.onLoginError();
return;
}
// If it's not prolonged there's nothing to do.
if (!this.isProlongedReadingListError()) {
this.log.debug("onRLSyncError has a non-authentication, non-prolonged error, so not showing any error UI");
return;
}
// So it's a prolonged error.
// Unfortunate duplication from below...
this.log.debug("onRLSyncError has a prolonged error");
let title = this._stringBundle.GetStringFromName("error.sync.title");
// XXX - this is somewhat wrong - we are reporting the threshold we consider
// to be prolonged, not how long it actually has been. (ie, lastSync below
// is effectively constant) - bit it too is copied from below.
let lastSync =
Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
let description =
this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
let priority = Weave.Notifications.PRIORITY_INFO;
let buttons = [
new Weave.NotificationButton(
this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
function() { gSyncUI.doSync(); return true; }
),
];
let notification =
new Weave.Notification(title, description, null, priority, buttons);
Weave.Notifications.replaceTitle(notification);
this.updateUI();
},
onSyncError: function SUI_onSyncError() {
this.log.debug("onSyncError: login=${login}, sync=${sync}", Weave.Status);
let title = this._stringBundle.GetStringFromName("error.sync.title");
@ -637,21 +543,17 @@ let gSyncUI = {
switch (topic) {
case "weave:service:sync:start":
case "weave:service:login:start":
case "readinglist:sync:start":
this.onActivityStart();
break;
case "weave:service:sync:finish":
case "weave:service:sync:error":
case "weave:service:login:finish":
case "weave:service:login:error":
case "readinglist:sync:finish":
case "readinglist:sync:error":
this.onActivityStop();
break;
}
// Now non-activity state (eg, enabled, errors, etc)
// Note that sync uses the ":ui:" notifications for errors because sync.
// ReadingList has no such concept (yet?; hopefully the :error is enough!)
switch (topic) {
case "weave:ui:sync:finish":
this.onSyncFinish();
@ -689,13 +591,6 @@ let gSyncUI = {
case "weave:ui:clear-error":
this.clearError();
break;
case "readinglist:sync:error":
this.onRLSyncError();
break;
case "readinglist:sync:finish":
this.clearError();
break;
}
},

Просмотреть файл

@ -274,7 +274,6 @@ let gInitialPages = [
#include browser-loop.js
#include browser-places.js
#include browser-plugins.js
#include browser-readinglist.js
#include browser-safebrowsing.js
#include browser-sidebar.js
#include browser-social.js
@ -1267,8 +1266,6 @@ var gBrowserInit = {
#ifdef E10S_TESTING_ONLY
gRemoteTabsUI.init();
#endif
ReadingListUI.init();
// Initialize the full zoom setting.
// We do this before the session restore service gets initialized so we can
// apply full zoom settings to tabs restored by the session restore service.
@ -1549,8 +1546,6 @@ var gBrowserInit = {
gMenuButtonUpdateBadge.uninit();
ReadingListUI.uninit();
SidebarUI.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
@ -2549,8 +2544,6 @@ function UpdatePageProxyState()
function SetPageProxyState(aState)
{
BookmarkingUI.onPageProxyStateChanged(aState);
ReadingListUI.onPageProxyStateChanged(aState);
if (!gURLBar)
return;

Просмотреть файл

@ -784,10 +784,6 @@
hidden="true"
tooltiptext="&pageReportIcon.tooltip;"
onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
<image id="readinglist-addremove-button"
class="urlbar-icon"
hidden="true"
onclick="ReadingListUI.buttonClick(event);"/>
<image id="reader-mode-button"
class="urlbar-icon"
hidden="true"
@ -918,22 +914,6 @@
new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS',
PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
</menu>
<menuseparator>
<observes element="readingListSidebar" attribute="hidden"/>
</menuseparator>
<menu id="BMB_readingList"
class="menu-iconic bookmark-item subviewbutton"
label="&readingList.label;"
container="true">
<observes element="readingListSidebar" attribute="hidden"/>
<menupopup id="BMB_readingListPopup"
placespopup="true"
onpopupshowing="ReadingListUI.onReadingListPopupShowing(this);">
<menuitem id="BMB_viewReadingListSidebar" class="subviewbutton"
oncommand="SidebarUI.show('readingListSidebar');"
label="&readingList.showSidebar.label;"/>
</menupopup>
</menu>
<menuseparator/>
<!-- Bookmarks menu items will go here -->
<menuitem id="BMB_bookmarksShowAll"

Просмотреть файл

@ -81,11 +81,22 @@ ContentSearchUIController.prototype = {
return this._defaultEngine;
},
set defaultEngine(val) {
this._defaultEngine = val;
set defaultEngine(engine) {
let icon;
if (engine.iconBuffer) {
icon = this._getFaviconURIFromBuffer(engine.iconBuffer);
}
else {
icon = this._getImageURIForCurrentResolution(
"chrome://mozapps/skin/places/defaultFavicon.png");
}
this._defaultEngine = {
name: engine.name,
icon: icon,
};
this._updateDefaultEngineHeader();
if (val && document.activeElement == this.input) {
if (engine && document.activeElement == this.input) {
this._speculativeConnect();
}
},
@ -96,10 +107,6 @@ ContentSearchUIController.prototype = {
set engines(val) {
this._engines = val;
if (!this._table.hidden) {
this._setUpOneOffButtons();
return;
}
this._pendingOneOffRefresh = true;
},
@ -127,6 +134,9 @@ ContentSearchUIController.prototype = {
let allElts = [...this._suggestionsList.children,
...this._oneOffButtons,
document.getElementById("contentSearchSettingsButton")];
// If we are selecting a suggestion and a one-off is selected, don't deselect it.
let excludeIndex = idx < this.numSuggestions && this.selectedButtonIndex > -1 ?
this.numSuggestions + this.selectedButtonIndex : -1;
for (let i = 0; i < allElts.length; ++i) {
let elt = allElts[i];
let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
@ -135,16 +145,43 @@ ContentSearchUIController.prototype = {
ariaSelectedElt.setAttribute("aria-selected", "true");
this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
}
else {
else if (i != excludeIndex) {
elt.classList.remove("selected");
ariaSelectedElt.setAttribute("aria-selected", "false");
}
}
},
get selectedButtonIndex() {
let elts = [...this._oneOffButtons,
document.getElementById("contentSearchSettingsButton")];
for (let i = 0; i < elts.length; ++i) {
if (elts[i].classList.contains("selected")) {
return i;
}
}
return -1;
},
set selectedButtonIndex(idx) {
let elts = [...this._oneOffButtons,
document.getElementById("contentSearchSettingsButton")];
for (let i = 0; i < elts.length; ++i) {
let elt = elts[i];
if (i == idx) {
elt.classList.add("selected");
elt.setAttribute("aria-selected", "true");
}
else {
elt.classList.remove("selected");
elt.setAttribute("aria-selected", "false");
}
}
},
get selectedEngineName() {
let selectedElt = this._table.querySelector(".selected");
if (selectedElt && selectedElt.engineName) {
let selectedElt = this._oneOffsTable.querySelector(".selected");
if (selectedElt) {
return selectedElt.engineName;
}
return this.defaultEngine.name;
@ -194,7 +231,7 @@ ContentSearchUIController.prototype = {
},
_onCommand: function(aEvent) {
if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
if (this.selectedButtonIndex == this._oneOffButtons.length) {
// Settings button was selected.
this._sendMsg("ManageEngines");
return;
@ -264,19 +301,58 @@ ContentSearchUIController.prototype = {
_onKeypress: function (event) {
let selectedIndexDelta = 0;
let selectedSuggestionDelta = 0;
let selectedOneOffDelta = 0;
switch (event.keyCode) {
case event.DOM_VK_UP:
if (!this._table.hidden) {
selectedIndexDelta = -1;
if (this._table.hidden) {
return;
}
if (event.getModifierState("Accel")) {
if (event.shiftKey) {
selectedSuggestionDelta = -1;
break;
}
this._cycleCurrentEngine(true);
break;
}
if (event.altKey) {
selectedOneOffDelta = -1;
break;
}
selectedIndexDelta = -1;
break;
case event.DOM_VK_DOWN:
if (this._table.hidden) {
this._getSuggestions();
return;
}
else {
selectedIndexDelta = 1;
if (event.getModifierState("Accel")) {
if (event.shiftKey) {
selectedSuggestionDelta = 1;
break;
}
this._cycleCurrentEngine(false);
break;
}
if (event.altKey) {
selectedOneOffDelta = 1;
break;
}
selectedIndexDelta = 1;
break;
case event.DOM_VK_TAB:
if (this._table.hidden) {
return;
}
// Shift+tab when either the first or no one-off is selected, as well as
// tab when the settings button is selected, should change focus as normal.
if ((this.selectedButtonIndex <= 0 && event.shiftKey) ||
this.selectedButtonIndex == this._oneOffButtons.length && !event.shiftKey) {
return;
}
selectedOneOffDelta = event.shiftKey ? -1 : 1;
break;
case event.DOM_VK_RIGHT:
// Allow normal caret movement until the caret is at the end of the input.
@ -297,37 +373,89 @@ ContentSearchUIController.prototype = {
}
this._stickyInputValue = this.input.value;
this._hideSuggestions();
break;
return;
case event.DOM_VK_RETURN:
this._onCommand(event);
break;
return;
case event.DOM_VK_DELETE:
if (this.selectedIndex >= 0) {
this.deleteSuggestionAtIndex(this.selectedIndex);
}
break;
return;
case event.DOM_VK_ESCAPE:
if (!this._table.hidden) {
this._hideSuggestions();
}
return;
default:
return;
}
let currentIndex = this.selectedIndex;
if (selectedIndexDelta) {
// Update the selection.
let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
let newSelectedIndex = currentIndex + selectedIndexDelta;
if (newSelectedIndex < -1) {
newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
}
else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
// If are moving up from the first one off, we have to deselect the one off
// manually because the selectedIndex setter tries to exclude the selected
// one-off (which is desirable for accel+shift+up/down).
if (currentIndex == this.numSuggestions && selectedIndexDelta == -1) {
this.selectedButtonIndex = -1;
}
this.selectAndUpdateInput(newSelectedIndex);
}
else if (selectedSuggestionDelta) {
let newSelectedIndex;
if (currentIndex >= this.numSuggestions || currentIndex == -1) {
// No suggestion already selected, select the first/last one appropriately.
newSelectedIndex = selectedSuggestionDelta == 1 ?
0 : this.numSuggestions - 1;
}
else {
newSelectedIndex = currentIndex + selectedSuggestionDelta;
}
if (newSelectedIndex >= this.numSuggestions) {
newSelectedIndex = -1;
}
this.selectAndUpdateInput(newSelectedIndex);
// Prevent the input's caret from moving.
event.preventDefault();
}
else if (selectedOneOffDelta) {
let newSelectedIndex;
let currentButton = this.selectedButtonIndex;
if (currentButton == -1 || currentButton == this._oneOffButtons.length) {
// No one-off already selected, select the first/last one appropriately.
newSelectedIndex = selectedOneOffDelta == 1 ?
0 : this._oneOffButtons.length - 1;
}
else {
newSelectedIndex = currentButton + selectedOneOffDelta;
}
// Allow selection of the settings button via the tab key.
if (newSelectedIndex == this._oneOffButtons.length &&
event.keyCode != event.DOM_VK_TAB) {
newSelectedIndex = -1;
}
this.selectedButtonIndex = newSelectedIndex;
}
// Prevent the input's caret from moving.
event.preventDefault();
},
_currentEngineIndex: -1,
_cycleCurrentEngine: function (aReverse) {
if ((this._currentEngineIndex == this._oneOffButtons.length - 1 && !aReverse) ||
(this._currentEngineIndex < 0 && aReverse)) {
return;
}
this._currentEngineIndex += aReverse ? -1 : 1;
let engineName = this._currentEngineIndex > -1 ?
this._oneOffButtons[this._currentEngineIndex].engineName :
this._originalDefaultEngine.name;
this._sendMsg("SetCurrentEngine", engineName);
},
_onFocus: function () {
@ -356,7 +484,12 @@ ContentSearchUIController.prototype = {
},
_onMousemove: function (event) {
this.selectedIndex = this._indexOfTableItem(event.target);
let idx = this._indexOfTableItem(event.target);
if (idx >= this.numSuggestions) {
this.selectedButtonIndex = idx - this.numSuggestions;
return;
}
this.selectedIndex = idx;
},
_onMouseup: function (event) {
@ -366,6 +499,15 @@ ContentSearchUIController.prototype = {
this._onCommand(event);
},
_onMouseout: function (event) {
// We only deselect one-off buttons and the settings button when they are
// moused out.
let idx = this._indexOfTableItem(event.originalTarget);
if (idx >= this.numSuggestions) {
this.selectedButtonIndex = -1;
}
},
_onClick: function (event) {
this._onMouseup(event);
},
@ -427,15 +569,22 @@ ContentSearchUIController.prototype = {
}
this._table.hidden = false;
this.input.setAttribute("aria-expanded", "true");
this._originalDefaultEngine = {
name: this.defaultEngine.name,
icon: this.defaultEngine.icon,
};
}
},
_onMsgState: function (state) {
this.defaultEngine = {
name: state.currentEngine.name,
icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
};
this.engines = state.engines;
// No point updating the default engine (and the header) if there's no change.
if (this.defaultEngine &&
this.defaultEngine.name == state.currentEngine.name &&
this.defaultEngine.icon == state.currentEngine.icon) {
return;
}
this.defaultEngine = state.currentEngine;
},
_onMsgCurrentState: function (state) {
@ -443,14 +592,7 @@ ContentSearchUIController.prototype = {
},
_onMsgCurrentEngine: function (engine) {
this.defaultEngine = {
name: engine.name,
icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
};
if (!this._table.hidden) {
this._setUpOneOffButtons();
return;
}
this.defaultEngine = engine;
this._pendingOneOffRefresh = true;
},
@ -464,9 +606,7 @@ ContentSearchUIController.prototype = {
_updateDefaultEngineHeader: function () {
let header = document.getElementById("contentSearchDefaultEngineHeader");
if (this.defaultEngine.icon) {
header.firstChild.setAttribute("src", this.defaultEngine.icon);
}
header.firstChild.setAttribute("src", this.defaultEngine.icon);
if (!this._strings) {
return;
}
@ -545,6 +685,14 @@ ContentSearchUIController.prototype = {
return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
},
// Adds "@2x" to the name of the given PNG url for "retina" screens.
_getImageURIForCurrentResolution: function (uri) {
if (window.devicePixelRatio > 1) {
return uri.replace(/\.png$/, "@2x.png");
}
return uri;
},
_getSearchEngines: function () {
this._sendMsg("GetState");
},
@ -572,6 +720,9 @@ ContentSearchUIController.prototype = {
_hideSuggestions: function () {
this.input.setAttribute("aria-expanded", "false");
this.selectedIndex = -1;
this.selectedButtonIndex = -1;
this._currentEngineIndex = -1;
this._table.hidden = true;
},
@ -605,11 +756,7 @@ ContentSearchUIController.prototype = {
document.addEventListener("mouseup", () => { delete this._mousedown; });
// Deselect the selected element on mouseout if it wasn't a suggestion.
this._table.addEventListener("mouseout", () => {
if (this.selectedIndex >= this.numSuggestions) {
this.selectAndUpdateInput(-1);
}
});
this._table.addEventListener("mouseout", this);
// If a search is loaded in the same tab, ensure the suggestions dropdown
// is hidden immediately when the page starts loading and not when it first
@ -620,9 +767,8 @@ ContentSearchUIController.prototype = {
let header = document.createElementNS(HTML_NS, "td");
headerRow.setAttribute("class", "contentSearchHeaderRow");
header.setAttribute("class", "contentSearchHeader");
let img = document.createElementNS(HTML_NS, "img");
img.setAttribute("src", "chrome://browser/skin/search-engine-placeholder.png");
header.appendChild(img);
let iconImg = document.createElementNS(HTML_NS, "img");
header.appendChild(iconImg);
header.id = "contentSearchDefaultEngineHeader";
headerRow.appendChild(header);
headerRow.addEventListener("click", this);
@ -707,10 +853,14 @@ ContentSearchUIController.prototype = {
let button = document.createElementNS(HTML_NS, "button");
button.setAttribute("class", "contentSearchOneOffItem");
let img = document.createElementNS(HTML_NS, "img");
let uri = "chrome://browser/skin/search-engine-placeholder.png";
let uri;
if (engine.iconBuffer) {
uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
}
else {
uri = this._getImageURIForCurrentResolution(
"chrome://browser/skin/search-engine-placeholder.png");
}
img.setAttribute("src", uri);
button.appendChild(img);
button.style.width = buttonWidth + "px";

Просмотреть файл

@ -6,15 +6,6 @@
Components.utils.import("resource://gre/modules/Services.jsm");
addEventListener("load", function () {
// unhide the reading-list engine if readinglist is enabled (note this
// dialog is only used with FxA sync, so no special action is needed
// for legacy sync.)
if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
document.getElementById("readinglist-engine").removeAttribute("hidden");
}
});
addEventListener("dialogaccept", function () {
let pane = document.getElementById("sync-customize-pane");
// First determine what the preference for the "global" sync enabled pref

Просмотреть файл

@ -27,8 +27,6 @@
<preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
<preference id="engine.addons" name="services.sync.engine.addons" type="bool"/>
<preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/>
<!-- non Sync-Engine engines -->
<preference id="engine.readinglist" name="readinglist.scheduler.enabled" type="bool"/>
</preferences>
<label id="sync-customize-title" value="&syncCustomize.title;"/>
@ -53,11 +51,6 @@
<checkbox label="&engine.history.label;"
accesskey="&engine.history.accesskey;"
preference="engine.history"/>
<checkbox id="readinglist-engine"
label="&engine.readinglist.label;"
accesskey="&engine.readinglist.accesskey;"
preference="engine.readinglist"
hidden="true"/>
<checkbox label="&engine.addons.label;"
accesskey="&engine.addons.accesskey;"
preference="engine.addons"/>

Просмотреть файл

@ -9,6 +9,7 @@ let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ExtensionContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
@ -656,3 +657,8 @@ let DOMFullscreenHandler = {
}
};
DOMFullscreenHandler.init();
ExtensionContent.init(this);
addEventListener("unload", () => {
ExtensionContent.uninit(this);
});

Просмотреть файл

@ -454,6 +454,7 @@ skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test un
[browser_urlbarSearchSingleWordNotification.js]
[browser_urlbarStop.js]
[browser_urlbarTrimURLs.js]
[browser_urlbar_autoFill_backspaced.js]
[browser_urlbar_search_healthreport.js]
[browser_urlbar_searchsettings.js]
[browser_utilityOverlay.js]

Просмотреть файл

@ -9,6 +9,8 @@ const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
const TEST_MSG = "ContentSearchUIControllerTest";
requestLongerTimeout(2);
add_task(function* emptyInput() {
yield setUp();
@ -102,7 +104,7 @@ add_task(function* rightLeftKeys() {
// trigger suggestions again and cycle through them by pressing Down until
// nothing is selected again.
state = yield msg("key", "VK_RIGHT");
checkState(state, "xfoo", [], 0);
checkState(state, "xfoo", [], -1);
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
@ -125,20 +127,202 @@ add_task(function* rightLeftKeys() {
yield msg("reset");
});
add_task(function* tabKey() {
yield setUp();
yield msg("key", { key: "x", waitForSuggestions: true });
let state = yield msg("key", "VK_TAB");
checkState(state, "x", ["xfoo", "xbar"], 2);
state = yield msg("key", "VK_TAB");
checkState(state, "x", ["xfoo", "xbar"], 3);
state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
checkState(state, "x", ["xfoo", "xbar"], 2);
state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
checkState(state, "x", [], -1);
yield setUp();
yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
for (let i = 0; i < 3; ++i) {
state = yield msg("key", "VK_TAB");
}
checkState(state, "x", [], -1);
yield setUp();
yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
state = yield msg("key", "VK_DOWN");
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
state = yield msg("key", "VK_TAB");
checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);
state = yield msg("key", "VK_TAB");
checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);
state = yield msg("key", "VK_DOWN");
checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
state = yield msg("key", "VK_DOWN");
checkState(state, "x", ["xfoo", "xbar"], 2);
state = yield msg("key", "VK_UP");
checkState(state, "xbar", ["xfoo", "xbar"], 1);
state = yield msg("key", "VK_TAB");
checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
state = yield msg("key", "VK_TAB");
checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
state = yield msg("key", "VK_TAB");
checkState(state, "xbar", [], -1);
yield msg("reset");
});
add_task(function* cycleSuggestions() {
yield setUp();
yield msg("key", { key: "x", waitForSuggestions: true });
let cycle = Task.async(function* (aSelectedButtonIndex) {
let modifiers = {
shiftKey: true,
accelKey: true,
};
let state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
});
yield cycle();
// Repeat with a one-off selected.
let state = yield msg("key", "VK_TAB");
checkState(state, "x", ["xfoo", "xbar"], 2);
yield cycle(0);
// Repeat with the settings button selected.
state = yield msg("key", "VK_TAB");
checkState(state, "x", ["xfoo", "xbar"], 3);
yield cycle(1);
yield msg("reset");
});
add_task(function* cycleOneOffs() {
yield setUp();
yield msg("key", { key: "x", waitForSuggestions: true });
yield msg("addDuplicateOneOff");
let state = yield msg("key", "VK_DOWN");
state = yield msg("key", "VK_DOWN");
checkState(state, "xbar", ["xfoo", "xbar"], 1);
let modifiers = {
altKey: true,
};
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1);
// If the settings button is selected, pressing alt+up/down should select the
// last/first one-off respectively (and deselect the settings button).
yield msg("key", "VK_TAB");
yield msg("key", "VK_TAB");
state = yield msg("key", "VK_TAB"); // Settings button selected.
checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
state = yield msg("key", "VK_TAB");
checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
yield msg("removeLastOneOff");
yield msg("reset");
});
add_task(function* mouse() {
yield setUp();
let state = yield msg("key", { key: "x", waitForSuggestions: true });
checkState(state, "x", ["xfoo", "xbar"], -1);
for (let i = 0; i < 4; ++i) {
state = yield msg("mousemove", i);
checkState(state, "x", ["xfoo", "xbar"], i);
}
state = yield msg("mousemove", 0);
checkState(state, "x", ["xfoo", "xbar"], 0);
state = yield msg("mousemove", 1);
checkState(state, "x", ["xfoo", "xbar"], 1);
state = yield msg("mousemove", 2);
checkState(state, "x", ["xfoo", "xbar"], 1, 0);
state = yield msg("mousemove", 3);
checkState(state, "x", ["xfoo", "xbar"], 1, 1);
state = yield msg("mousemove", -1);
checkState(state, "x", ["xfoo", "xbar"], 1);
yield msg("reset");
yield setUp();
state = yield msg("key", { key: "x", waitForSuggestions: true });
checkState(state, "x", ["xfoo", "xbar"], -1);
state = yield msg("mousemove", 0);
checkState(state, "x", ["xfoo", "xbar"], 0);
state = yield msg("mousemove", 2);
checkState(state, "x", ["xfoo", "xbar"], 0, 0);
state = yield msg("mousemove", -1);
checkState(state, "x", ["xfoo", "xbar"], 0);
yield msg("reset");
});
@ -197,6 +381,34 @@ add_task(function* formHistory() {
yield msg("reset");
});
add_task(function* cycleEngines() {
yield setUp();
yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
let promiseEngineChange = function(newEngineName) {
let deferred = Promise.defer();
Services.obs.addObserver(function resolver(subj, topic, data) {
if (data != "engine-current") {
return;
}
SimpleTest.is(subj.name, newEngineName, "Engine cycled correctly");
Services.obs.removeObserver(resolver, "browser-search-engine-modified");
deferred.resolve();
}, "browser-search-engine-modified", false);
return deferred.promise;
}
let p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME);
yield msg("key", { key: "VK_DOWN", modifiers: { accelKey: true }});
yield p;
p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME);
yield msg("key", { key: "VK_UP", modifiers: { accelKey: true }});
yield p;
yield msg("reset");
});
add_task(function* search() {
yield setUp();
@ -297,6 +509,42 @@ add_task(function* search() {
yield promiseTab();
yield setUp();
// Test selecting a suggestion, then clicking a one-off without deselecting the
// suggestion.
yield msg("key", { key: "x", waitForSuggestions: true });
p = msg("waitForSearch");
yield msg("mousemove", 1);
yield msg("mousemove", 3);
yield msg("click", { eltIdx: 3, modifiers: modifiers });
mesg = yield p;
eventData.searchString = "xfoo"
eventData.selection = {
index: 1,
kind: "mouse",
};
SimpleTest.isDeeply(eventData, mesg, "Search event data");
yield promiseTab();
yield setUp();
// Same as above, but with the keyboard.
delete modifiers.button;
yield msg("key", { key: "x", waitForSuggestions: true });
p = msg("waitForSearch");
yield msg("key", "VK_DOWN");
yield msg("key", "VK_DOWN");
yield msg("key", "VK_TAB");
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
mesg = yield p;
eventData.selection = {
index: 1,
kind: "key",
};
SimpleTest.isDeeply(eventData, mesg, "Search event data");
yield promiseTab();
yield setUp();
// Test searching when using IME composition.
let state = yield msg("startComposition", { data: "" });
checkState(state, "", [], -1);
@ -308,8 +556,10 @@ add_task(function* search() {
p = msg("waitForSearch");
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
mesg = yield p;
eventData.searchString = "x"
eventData.originalEvent = modifiers;
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
delete eventData.selection;
SimpleTest.isDeeply(eventData, mesg, "Search event data");
yield promiseTab();
@ -428,7 +678,7 @@ function msg(type, data=null) {
}
function checkState(actualState, expectedInputVal, expectedSuggestions,
expectedSelectedIdx) {
expectedSelectedIdx, expectedSelectedButtonIdx) {
expectedSuggestions = expectedSuggestions.map(sugg => {
return typeof(sugg) == "object" ? sugg : {
str: sugg,
@ -436,6 +686,10 @@ function checkState(actualState, expectedInputVal, expectedSuggestions,
};
});
if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
expectedSelectedIdx = expectedSuggestions.length + expectedSelectedButtonIdx;
}
let expectedState = {
selectedIndex: expectedSelectedIdx,
numSuggestions: expectedSuggestions.length,
@ -448,6 +702,15 @@ function checkState(actualState, expectedInputVal, expectedSuggestions,
inputValue: expectedInputVal,
ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
};
if (expectedSelectedButtonIdx != undefined) {
expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
}
else if (expectedSelectedIdx < expectedSuggestions.length) {
expectedState.selectedButtonIndex = -1;
}
else {
expectedState.selectedButtonIndex = expectedSelectedIdx - expectedSuggestions.length;
}
SimpleTest.isDeeply(actualState, expectedState, "State");
}

Просмотреть файл

@ -4,13 +4,10 @@
/**
* Test that the reader mode button appears and works properly on
* reader-able content, and that ReadingList button can open and close
* its Sidebar UI.
* reader-able content.
*/
const TEST_PREFS = [
["reader.parse-on-load.enabled", true],
["browser.readinglist.enabled", true],
["browser.readinglist.introShown", false],
];
const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
@ -63,26 +60,6 @@ add_task(function* test_reader_button() {
is(gURLBar.value, readerUrl, "gURLBar value is about:reader URL");
is(gURLBar.textValue, url.substring("http://".length), "gURLBar is displaying original article URL");
// Readinglist button should be present, and status should be "openned", as the
// first time in readerMode opens the Sidebar ReadingList as a feature introduction.
let listButton;
yield promiseWaitForCondition(() =>
listButton = gBrowser.contentDocument.getElementById("list-button"));
is_element_visible(listButton, "List button is present on a reader-able page");
yield promiseWaitForCondition(() => listButton.classList.contains("on"));
ok(listButton.classList.contains("on"),
"List button should indicate SideBar-ReadingList open.");
ok(ReadingListUI.isSidebarOpen,
"The ReadingListUI should indicate SideBar-ReadingList open.");
// Now close the Sidebar ReadingList.
listButton.click();
yield promiseWaitForCondition(() => !listButton.classList.contains("on"));
ok(!listButton.classList.contains("on"),
"List button should now indicate SideBar-ReadingList closed.");
ok(!ReadingListUI.isSidebarOpen,
"The ReadingListUI should now indicate SideBar-ReadingList closed.");
// Switch page back out of reader mode.
readerButton.click();
yield promiseTabLoadEvent(tab);

Просмотреть файл

@ -4,8 +4,7 @@
/**
* Test that the reader mode button appears and works properly on
* reader-able content, and that ReadingList button can open and close
* its Sidebar UI.
* reader-able content.
*/
const TEST_PREFS = [
["reader.parse-on-load.enabled", true],

Просмотреть файл

@ -4,8 +4,6 @@
let {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
let {Weave} = Cu.import("resource://services-sync/main.js", {});
let {Notifications} = Cu.import("resource://services-sync/notifications.js", {});
// The BackStagePass allows us to get this test-only non-exported function.
let {getInternalScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {});
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
@ -37,23 +35,6 @@ add_task(function* prepare() {
});
});
add_task(function* testNotProlongedRLErrorWhenDisabled() {
// Here we arrange for the (dead?) readinglist scheduler to have a last-synced
// date of long ago and the RL scheduler is disabled.
// gSyncUI.isProlongedReadingListError() should return false.
// Pretend the reading-list is in the "prolonged error" state.
let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
// It's prolonged while it's enabled.
Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
Assert.equal(gSyncUI.isProlongedReadingListError(), true);
// But false when disabled.
Services.prefs.setBoolPref("readinglist.scheduler.enabled", false);
Assert.equal(gSyncUI.isProlongedReadingListError(), false);
});
add_task(function* testProlongedSyncError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
@ -76,32 +57,6 @@ add_task(function* testProlongedSyncError() {
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
add_task(function* testProlongedRLError() {
Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend the reading-list is in the "prolonged error" state.
let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_OTHER;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now pretend we just had a successful sync - the error notification should go away.
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.prefs.setCharPref("readinglist.scheduler.lastSync", Date.now().toString());
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
add_task(function* testSyncLoginError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
@ -155,13 +110,7 @@ add_task(function* testSyncLoginNetworkError() {
Services.obs.notifyObservers(null, "weave:ui:login:error", null);
Assert.ok(sawNotificationAdded);
// clear the notification.
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
// cool - so reset the flag and test what should *not* show an error.
// reset the flag and test what should *not* show an error.
sawNotificationAdded = false;
Weave.Status.sync = Weave.LOGIN_FAILED;
Weave.Status.login = Weave.LOGIN_FAILED_NETWORK_ERROR;
@ -179,80 +128,6 @@ add_task(function* testSyncLoginNetworkError() {
}
});
add_task(function* testRLLoginError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend RL is in an auth error state
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now pretend we just had a successful sync - the error notification should go away.
getInternalScheduler().state = ReadingListScheduler.STATE_OK;
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
// Here we put readinglist into an "authentication error" state (should see
// the error bar reflecting this), then report a prolonged error from Sync (an
// infobar to reflect the sync error should replace it), then resolve the sync
// error - the authentication error from readinglist should remain.
add_task(function* testRLLoginErrorRemains() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend RL is in an auth error state
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now Sync into a prolonged auth error state.
promiseNotificationAdded = promiseObserver("weave:notification:added");
Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE;
Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
subject = yield promiseNotificationAdded;
// still exactly 1 notification with the "login" title.
notification = subject.wrappedJSObject.object;
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Resolve the sync problem.
promiseNotificationAdded = promiseObserver("weave:notification:added");
Weave.Status.sync = Weave.STATUS_OK;
Weave.Status.login = Weave.LOGIN_SUCCEEDED;
Services.obs.notifyObservers(null, "weave:ui:sync:finish", null);
// Expect one notification - the RL login problem.
subject = yield promiseNotificationAdded;
// still exactly 1 notification with the "login" title.
notification = subject.wrappedJSObject.object;
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// and cleanup - resolve the readinglist error.
getInternalScheduler().state = ReadingListScheduler.STATE_OK;
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
function checkButtonsStatus(shouldBeActive) {
let button = document.getElementById("sync-button");
let fxaContainer = document.getElementById("PanelUI-footer-fxa");
@ -287,26 +162,12 @@ add_task(function* testButtonActivities() {
testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
testButtonActions("weave:service:sync:start", "weave:service:sync:error");
testButtonActions("readinglist:sync:start", "readinglist:sync:finish");
testButtonActions("readinglist:sync:start", "readinglist:sync:error");
// and ensure the counters correctly handle multiple in-flight syncs
Services.obs.notifyObservers(null, "weave:service:sync:start", null);
checkButtonsStatus(true);
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
checkButtonsStatus(true);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
// sync is still going...
checkButtonsStatus(true);
// another reading list starts
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
checkButtonsStatus(true);
// The initial sync stops.
// sync stops.
Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
// RL is still going...
checkButtonsStatus(true);
// RL finishes with an error, so no longer active.
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
// Button should not be active.
checkButtonsStatus(false);
} finally {
PanelUI.hide();

Просмотреть файл

@ -0,0 +1,148 @@
/* This test ensures that backspacing autoFilled values still allows to
* confirm the remaining value.
*/
function* test_autocomplete(data) {
let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
info(desc);
yield promiseAutocompleteResultPopup(typed);
is(gURLBar.value, autofilled, "autofilled value is as expected");
if (onAutoFill)
onAutoFill()
keys.forEach(key => EventUtils.synthesizeKey(key, {}));
is(gURLBar.value, modified, "backspaced value is as expected");
yield promiseSearchComplete();
ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
let result = gURLBar.popup.richlistbox.children[0];
let type = result.getAttribute("type");
let types = type.split(/\s+/);
ok(types.includes(action), `The type attribute "${type}" includes the expected action "${action}"`);
gURLBar.popup.hidePopup();
yield promisePopupHidden(gURLBar.popup);
gURLBar.blur();
};
add_task(function* () {
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
Services.prefs.clearUserPref("browser.urlbar.autoFill");
gURLBar.handleRevert();
yield PlacesTestUtils.clearHistory();
});
Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
// Add a typed visit, so it will be autofilled.
yield PlacesTestUtils.addVisits({
uri: NetUtil.newURI("http://example.com/"),
transition: Ci.nsINavHistoryService.TRANSITION_TYPED
});
yield test_autocomplete({ desc: "DELETE the autofilled part should search",
typed: "exam",
autofilled: "example.com/",
modified: "exam",
keys: ["VK_DELETE"],
action: "searchengine"
});
yield test_autocomplete({ desc: "DELETE the final slash should visit",
typed: "example.com",
autofilled: "example.com/",
modified: "example.com",
keys: ["VK_DELETE"],
action: "visiturl"
});
yield test_autocomplete({ desc: "BACK_SPACE the autofilled part should search",
typed: "exam",
autofilled: "example.com/",
modified: "exam",
keys: ["VK_BACK_SPACE"],
action: "searchengine"
});
yield test_autocomplete({ desc: "BACK_SPACE the final slash should visit",
typed: "example.com",
autofilled: "example.com/",
modified: "example.com",
keys: ["VK_BACK_SPACE"],
action: "visiturl"
});
yield test_autocomplete({ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
typed: "exam",
autofilled: "example.com/",
modified: "exa",
keys: ["VK_DELETE", "VK_BACK_SPACE"],
action: "searchengine"
});
yield test_autocomplete({ desc: "DELETE the final slash, then BACK_SPACE, should search",
typed: "example.com",
autofilled: "example.com/",
modified: "example.co",
keys: ["VK_DELETE", "VK_BACK_SPACE"],
action: "visiturl"
});
yield test_autocomplete({ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
typed: "exam",
autofilled: "example.com/",
modified: "exa",
keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
action: "searchengine"
});
yield test_autocomplete({ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
typed: "example.com",
autofilled: "example.com/",
modified: "example.co",
keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
action: "visiturl"
});
yield test_autocomplete({ desc: "BACK_SPACE after blur should search",
typed: "ex",
autofilled: "example.com/",
modified: "e",
keys: ["VK_BACK_SPACE"],
action: "searchengine",
onAutoFill: () => {
gURLBar.blur();
gURLBar.focus();
gURLBar.selectionStart = 1;
gURLBar.selectionEnd = 12;
}
});
yield test_autocomplete({ desc: "DELETE after blur should search",
typed: "ex",
autofilled: "example.com/",
modified: "e",
keys: ["VK_DELETE"],
action: "searchengine",
onAutoFill: () => {
gURLBar.blur();
gURLBar.focus();
gURLBar.selectionStart = 1;
gURLBar.selectionEnd = 12;
}
});
yield test_autocomplete({ desc: "double BACK_SPACE after blur should search",
typed: "ex",
autofilled: "example.com/",
modified: "e",
keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
action: "searchengine",
onAutoFill: () => {
gURLBar.blur();
gURLBar.focus();
gURLBar.selectionStart = 2;
gURLBar.selectionEnd = 12;
}
});
yield PlacesTestUtils.clearHistory();
});

Просмотреть файл

@ -96,8 +96,11 @@ let messageHandlers = {
type: "mousemove",
clickcount: 0,
}
row.addEventListener("mousemove", function handler() {
row.removeEventListener("mousemove", handler);
ack("mousemove");
});
content.synthesizeMouseAtCenter(row, event);
ack("mousemove");
},
click: function (arg) {
@ -124,11 +127,27 @@ let messageHandlers = {
ack("addInputValueToFormHistory");
},
addDuplicateOneOff: function () {
let btn = gController._oneOffButtons[gController._oneOffButtons.length - 1];
let newBtn = btn.cloneNode(true);
btn.parentNode.appendChild(newBtn);
gController._oneOffButtons.push(newBtn);
ack("addDuplicateOneOff");
},
removeLastOneOff: function () {
gController._oneOffButtons.pop().remove();
ack("removeLastOneOff");
},
reset: function () {
// Reset both the input and suggestions by select all + delete.
// Reset both the input and suggestions by select all + delete. If there was
// no text entered, this won't have any effect, so also escape to ensure the
// suggestions table is closed.
gController.input.focus();
content.synthesizeKey("a", { accelKey: true });
content.synthesizeKey("VK_DELETE", {});
content.synthesizeKey("VK_ESCAPE", {});
ack("reset");
},
};
@ -165,6 +184,7 @@ function waitForContentSearchEvent(messageType, cb) {
function currentState() {
let state = {
selectedIndex: gController.selectedIndex,
selectedButtonIndex: gController.selectedButtonIndex,
numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
suggestionAtIndex: [],
isFormHistorySuggestionAtIndex: [],

Просмотреть файл

@ -139,17 +139,6 @@
label="&unsortedBookmarksCmd.label;"
class="subviewbutton cui-withicon"
oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
<toolbarseparator>
<observes element="readingListSidebar" attribute="hidden"/>
</toolbarseparator>
<toolbarbutton id="panelMenu_viewReadingListSidebar"
label="&readingList.showSidebar.label;"
class="subviewbutton"
key="key_readingListSidebar"
oncommand="SidebarUI.toggle('readingListSidebar'); PanelUI.hide();">
<observes element="readingListSidebar" attribute="checked"/>
<observes element="readingListSidebar" attribute="hidden"/>
</toolbarbutton>
<toolbarseparator class="small-separator"/>
<toolbaritem id="panelMenu_bookmarksMenu"
orient="vertical"

20
browser/components/extensions/bootstrap.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,20 @@
/* 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";
Components.utils.import("resource://gre/modules/Extension.jsm");
let extension;
function startup(data, reason)
{
extension = new Extension(data);
extension.startup();
}
function shutdown(data, reason)
{
extension.shutdown();
}

Просмотреть файл

@ -0,0 +1,326 @@
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
let {
EventManager,
DefaultWeakMap,
ignoreEvent,
runSafe,
} = ExtensionUtils;
// WeakMap[Extension -> BrowserAction]
let browserActionMap = new WeakMap();
function browserActionOf(extension)
{
return browserActionMap.get(extension);
}
function makeWidgetId(id)
{
id = id.toLowerCase();
return id.replace(/[^a-z0-9_-]/g, "_");
}
let nextActionId = 0;
// Responsible for the browser_action section of the manifest as well
// as the associated popup.
function BrowserAction(options, extension)
{
this.extension = extension;
this.id = makeWidgetId(extension.id) + "-browser-action";
this.widget = null;
this.title = new DefaultWeakMap(extension.localize(options.default_title));
this.badgeText = new DefaultWeakMap();
this.badgeBackgroundColor = new DefaultWeakMap();
this.icon = new DefaultWeakMap(options.default_icon);
this.popup = new DefaultWeakMap(options.default_popup);
// Make the default something that won't compare equal to anything.
this.prevPopups = new DefaultWeakMap({});
this.context = null;
}
BrowserAction.prototype = {
build() {
let widget = CustomizableUI.createWidget({
id: this.id,
type: "custom",
removable: true,
defaultArea: CustomizableUI.AREA_NAVBAR,
onBuild: document => {
let node = document.createElement("toolbarbutton");
node.id = this.id;
node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
node.setAttribute("constrain-size", "true");
this.updateTab(null, node);
let tabbrowser = document.defaultView.gBrowser;
tabbrowser.ownerDocument.addEventListener("TabSelect", () => {
this.updateTab(tabbrowser.selectedTab, node);
});
node.addEventListener("command", event => {
if (node.getAttribute("type") != "panel") {
this.emit("click");
}
});
return node;
},
});
this.widget = widget;
},
// Initialize the toolbar icon and popup given that |tab| is the
// current tab and |node| is the CustomizableUI node. Note: |tab|
// will be null if we don't know the current tab yet (during
// initialization).
updateTab(tab, node) {
let window = node.ownerDocument.defaultView;
let title = this.getProperty(tab, "title");
if (title) {
node.setAttribute("tooltiptext", title);
node.setAttribute("label", title);
} else {
node.removeAttribute("tooltiptext");
node.removeAttribute("label");
}
let badgeText = this.badgeText.get(tab);
if (badgeText) {
node.setAttribute("badge", badgeText);
} else {
node.removeAttribute("badge");
}
function toHex(n) {
return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
}
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
'class', 'toolbarbutton-badge');
if (badgeNode) {
let color = this.badgeBackgroundColor.get(tab);
if (Array.isArray(color)) {
color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
}
badgeNode.style.backgroundColor = color;
}
let iconURL = this.getIcon(tab, node);
node.setAttribute("image", iconURL);
let popup = this.getProperty(tab, "popup");
if (popup != this.prevPopups.get(window)) {
this.prevPopups.set(window, popup);
let panel = node.querySelector("panel");
if (panel) {
panel.remove();
}
if (popup) {
let popupURL = this.extension.baseURI.resolve(popup);
node.setAttribute("type", "panel");
let document = node.ownerDocument;
let panel = document.createElement("panel");
panel.setAttribute("class", "browser-action-panel");
panel.setAttribute("type", "arrow");
panel.setAttribute("flip", "slide");
node.appendChild(panel);
let browser = document.createElementNS(XUL_NS, "browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("width", "500");
browser.setAttribute("height", "500");
panel.appendChild(browser);
let loadListener = () => {
panel.removeEventListener("load", loadListener);
if (this.context) {
this.context.unload();
}
this.context = new ExtensionPage(this.extension, {
type: "popup",
contentWindow: browser.contentWindow,
uri: Services.io.newURI(popupURL, null, null),
docShell: browser.docShell,
});
GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
browser.setAttribute("src", popupURL);
};
panel.addEventListener("load", loadListener);
} else {
node.removeAttribute("type");
}
}
},
// Note: tab is allowed to be null here.
getIcon(tab, node) {
let icon = this.icon.get(tab);
let url;
if (typeof(icon) != "object") {
url = icon;
} else {
let window = node.ownerDocument.defaultView;
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
let res = {value: 1}
utils.getResolution(res);
let size = res.value == 1 ? 19 : 38;
url = icon[size];
}
if (url) {
return this.extension.baseURI.resolve(url);
} else {
return "chrome://browser/content/extension.svg";
}
},
// Update the toolbar button for a given window.
updateWindow(window) {
let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
this.updateTab(tab, node);
},
// Update the toolbar button when the extension changes the icon,
// title, badge, etc. If it only changes a parameter for a single
// tab, |tab| will be that tab. Otherwise it will be null.
updateOnChange(tab) {
if (tab) {
if (tab.selected) {
this.updateWindow(tab.ownerDocument.defaultView);
}
} else {
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
if (window.gBrowser) {
this.updateWindow(window);
}
}
}
},
// tab is allowed to be null.
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
setProperty(tab, prop, value) {
this[prop].set(tab, value);
this.updateOnChange(tab);
},
// tab is allowed to be null.
// prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
getProperty(tab, prop) {
return this[prop].get(tab);
},
shutdown() {
CustomizableUI.destroyWidget(this.id);
},
};
EventEmitter.decorate(BrowserAction.prototype);
extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
let browserAction = new BrowserAction(manifest.browser_action, extension);
browserAction.build();
browserActionMap.set(extension, browserAction);
});
extensions.on("shutdown", (type, extension) => {
if (browserActionMap.has(extension)) {
browserActionMap.get(extension).shutdown();
browserActionMap.delete(extension);
}
});
extensions.registerAPI((extension, context) => {
return {
browserAction: {
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
let listener = () => {
let tab = TabManager.activeTab;
fire(TabManager.convert(extension, tab));
};
browserActionOf(extension).on("click", listener);
return () => {
browserActionOf(extension).off("click", listener);
};
}).api(),
setTitle: function(details) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
browserActionOf(extension).setProperty(tab, "title", details.title);
},
getTitle: function(details, callback) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
let title = browserActionOf(extension).getProperty(tab, "title");
runSafe(context, callback, title);
},
setIcon: function(details, callback) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
if (details.imageData) {
// FIXME: Support the imageData attribute.
return;
}
browserActionOf(extension).setProperty(tab, "icon", details.path);
},
setBadgeText: function(details) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
browserActionOf(extension).setProperty(tab, "badgeText", details.text);
},
getBadgeText: function(details, callback) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
let text = browserActionOf(extension).getProperty(tab, "badgeText");
runSafe(context, callback, text);
},
setPopup: function(details) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
browserActionOf(extension).setProperty(tab, "popup", details.popup);
},
getPopup: function(details, callback) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
let popup = browserActionOf(extension).getProperty(tab, "popup");
runSafe(context, callback, popup);
},
setBadgeBackgroundColor: function(details) {
let color = details.color;
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
},
getBadgeBackgroundColor: function(details, callback) {
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
runSafe(context, callback, color);
},
}
};
});

Просмотреть файл

@ -0,0 +1,8 @@
extensions.registerPrivilegedAPI("contextMenus", (extension, context) => {
return {
contextMenus: {
create() {},
removeAll() {},
},
};
});

Просмотреть файл

@ -0,0 +1,486 @@
XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
"resource:///modules/NewTabURL.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
let {
EventManager,
ignoreEvent,
runSafe,
} = ExtensionUtils;
// This function is pretty tightly tied to Extension.jsm.
// Its job is to fill in the |tab| property of the sender.
function getSender(context, target, sender)
{
// The message was sent from a content script to a <browser> element.
// We can just get the |tab| from |target|.
if (target instanceof Ci.nsIDOMXULElement) {
// The message came from a content script.
let tabbrowser = target.ownerDocument.defaultView.gBrowser;
if (!tabbrowser) {
return;
}
let tab = tabbrowser.getTabForBrowser(target);
sender.tab = TabManager.convert(context.extension, tab);
} else {
// The message came from an ExtensionPage. In that case, it should
// include a tabId property (which is filled in by the page-open
// listener below).
if ("tabId" in sender) {
sender.tab = TabManager.convert(context.extension, TabManager.getTab(sender.tabId));
delete sender.tabId;
}
}
}
// WeakMap[ExtensionPage -> {tab, parentWindow}]
let pageDataMap = new WeakMap();
// This listener fires whenever an extension page opens in a tab
// (either initiated by the extension or the user). Its job is to fill
// in some tab-specific details and keep data around about the
// ExtensionPage.
extensions.on("page-load", (type, page, params, sender, delegate) => {
if (params.type == "tab") {
let browser = params.docShell.chromeEventHandler;
let parentWindow = browser.ownerDocument.defaultView;
let tab = parentWindow.gBrowser.getTabForBrowser(browser);
sender.tabId = TabManager.getId(tab);
pageDataMap.set(page, {tab, parentWindow});
}
delegate.getSender = getSender;
});
extensions.on("page-unload", (type, page) => {
pageDataMap.delete(page);
});
extensions.on("page-shutdown", (type, page) => {
if (pageDataMap.has(page)) {
let {tab, parentWindow} = pageDataMap.get(page);
pageDataMap.delete(page);
parentWindow.gBrowser.removeTab(tab);
}
});
extensions.on("fill-browser-data", (type, browser, data, result) => {
let tabId = TabManager.getBrowserId(browser);
if (tabId == -1) {
result.cancel = true;
return;
}
data.tabId = tabId;
});
// TODO: activeTab permission
extensions.registerAPI((extension, context) => {
let self = {
tabs: {
onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
let tab = event.originalTarget;
let tabId = TabManager.getId(tab);
let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
fire({tabId, windowId});
}).api(),
onCreated: new EventManager(context, "tabs.onCreated", fire => {
let listener = event => {
let tab = event.originalTarget;
fire({tab: TabManager.convert(extension, tab)});
};
let windowListener = window => {
for (let tab of window.gBrowser.tabs) {
fire({tab: TabManager.convert(extension, tab)});
}
};
WindowListManager.addOpenListener(windowListener, false);
AllWindowEvents.addListener("TabOpen", listener);
return () => {
WindowListManager.removeOpenListener(windowListener);
AllWindowEvents.removeListener("TabOpen", listener);
};
}).api(),
onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
function sanitize(extension, changeInfo) {
let result = {};
let nonempty = false;
for (let prop in changeInfo) {
if ((prop != "favIconUrl" && prop != "url") || extension.hasPermission("tabs")) {
nonempty = true;
result[prop] = changeInfo[prop];
}
}
return [nonempty, result];
}
let listener = event => {
let tab = event.originalTarget;
let window = tab.ownerDocument.defaultView;
let tabId = TabManager.getId(tab);
let changeInfo = {};
let needed = false;
if (event.type == "TabAttrModified") {
if (event.detail.changed.indexOf("image") != -1) {
changeInfo.favIconUrl = window.gBrowser.getIcon(tab);
needed = true;
}
} else if (event.type == "TabPinned") {
changeInfo.pinned = true;
needed = true;
} else if (event.type == "TabUnpinned") {
changeInfo.pinned = false;
needed = true;
}
[needed, changeInfo] = sanitize(extension, changeInfo);
if (needed) {
fire(tabId, changeInfo, TabManager.convert(extension, tab));
}
};
let progressListener = {
onStateChange(browser, webProgress, request, stateFlags, statusCode) {
if (!webProgress.isTopLevel) {
return;
}
let status;
if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
status = "loading";
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
status = "complete";
}
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
statusCode == Cr.NS_BINDING_ABORTED) {
status = "complete";
}
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
let tabId = TabManager.getId(tab);
let [needed, changeInfo] = sanitize(extension, {status});
fire(tabId, changeInfo, TabManager.convert(extension, tab));
},
onLocationChange(browser, webProgress, request, locationURI, flags) {
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
let tabId = TabManager.getId(tab);
let [needed, changeInfo] = sanitize(extension, {url: locationURI.spec});
if (needed) {
fire(tabId, changeInfo, TabManager.convert(extension, tab));
}
},
};
AllWindowEvents.addListener("progress", progressListener);
AllWindowEvents.addListener("TabAttrModified", listener);
AllWindowEvents.addListener("TabPinned", listener);
AllWindowEvents.addListener("TabUnpinned", listener);
return () => {
AllWindowEvents.removeListener("progress", progressListener);
AllWindowEvents.addListener("TabAttrModified", listener);
AllWindowEvents.addListener("TabPinned", listener);
AllWindowEvents.addListener("TabUnpinned", listener);
};
}).api(),
onReplaced: ignoreEvent(),
onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
let tabListener = event => {
let tab = event.originalTarget;
let tabId = TabManager.getId(tab);
let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
let removeInfo = {windowId, isWindowClosing: false};
fire(tabId, removeInfo);
};
let windowListener = window => {
for (let tab of window.gBrowser.tabs) {
let tabId = TabManager.getId(tab);
let windowId = WindowManager.getId(window);
let removeInfo = {windowId, isWindowClosing: true};
fire(tabId, removeInfo);
}
};
WindowListManager.addCloseListener(windowListener);
AllWindowEvents.addListener("TabClose", tabListener);
return () => {
WindowListManager.removeCloseListener(windowListener);
AllWindowEvents.removeListener("TabClose", tabListener);
};
}).api(),
create: function(createProperties, callback) {
if (!createProperties) {
createProperties = {};
}
let url = createProperties.url || NewTabURL.get();
url = extension.baseURI.resolve(url);
function createInWindow(window) {
let tab = window.gBrowser.addTab(url);
let active = true;
if ("active" in createProperties) {
active = createProperties.active;
} else if ("selected" in createProperties) {
active = createProperties.selected;
}
if (active) {
window.gBrowser.selectedTab = tab;
}
if ("index" in createProperties) {
window.gBrowser.moveTabTo(tab, createProperties.index);
}
if (createProperties.pinned) {
window.gBrowser.pinTab(tab);
}
if (callback) {
runSafe(context, callback, TabManager.convert(extension, tab));
}
}
let window = createProperties.windowId ?
WindowManager.getWindow(createProperties.windowId) :
WindowManager.topWindow;
if (!window.gBrowser) {
let obs = (finishedWindow, topic, data) => {
if (finishedWindow != window) {
return;
}
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
createInWindow(window);
};
Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
} else {
createInWindow(window);
}
},
remove: function(tabs, callback) {
if (!Array.isArray(tabs)) {
tabs = [tabs];
}
for (let tabId of tabs) {
let tab = TabManager.getTab(tabId);
tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
}
if (callback) {
runSafe(context, callback);
}
},
update: function(...args) {
let tabId, updateProperties, callback;
if (args.length == 1) {
updateProperties = args[0];
} else {
[tabId, updateProperties, callback] = args;
}
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
let tabbrowser = tab.ownerDocument.gBrowser;
if ("url" in updateProperties) {
tab.linkedBrowser.loadURI(updateProperties.url);
}
if ("active" in updateProperties) {
if (updateProperties.active) {
tabbrowser.selectedTab = tab;
} else {
// Not sure what to do here? Which tab should we select?
}
}
if ("pinned" in updateProperties) {
if (updateProperties.pinned) {
tabbrowser.pinTab(tab);
} else {
tabbrowser.unpinTab(tab);
}
}
// FIXME: highlighted/selected, openerTabId
if (callback) {
runSafe(context, callback, TabManager.convert(extension, tab));
}
},
reload: function(tabId, reloadProperties, callback) {
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (reloadProperties && reloadProperties.bypassCache) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
}
tab.linkedBrowser.reloadWithFlags(flags);
if (callback) {
runSafe(context, callback);
}
},
get: function(tabId, callback) {
let tab = TabManager.getTab(tabId);
runSafe(context, callback, TabManager.convert(extension, tab));
},
getAllInWindow: function(...args) {
let window, callback;
if (args.length == 1) {
callbacks = args[0];
} else {
window = WindowManager.getWindow(args[0]);
callback = args[1];
}
if (!window) {
window = WindowManager.topWindow;
}
return self.tabs.query({windowId: WindowManager.getId(window)}, callback);
},
query: function(queryInfo, callback) {
if (!queryInfo) {
queryInfo = {};
}
function matches(window, tab) {
let props = ["active", "pinned", "highlighted", "status", "title", "url", "index"];
for (let prop of props) {
if (prop in queryInfo && queryInfo[prop] != tab[prop]) {
return false;
}
}
let lastFocused = window == WindowManager.topWindow;
if ("lastFocusedWindow" in queryInfo && queryInfo.lastFocusedWindow != lastFocused) {
return false;
}
let windowType = WindowManager.windowType(window);
if ("windowType" in queryInfo && queryInfo.windowType != windowType) {
return false;
}
if ("windowId" in queryInfo) {
if (queryInfo.windowId == WindowManager.WINDOW_ID_CURRENT) {
if (context.contentWindow != window) {
return false;
}
} else {
if (queryInfo.windowId != tab.windowId) {
return false;
}
}
}
if ("currentWindow" in queryInfo) {
let eq = window == context.contentWindow;
if (queryInfo.currentWindow != eq) {
return false;
}
}
return true;
}
let result = [];
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
let tabs = TabManager.getTabs(extension, window);
for (let tab of tabs) {
if (matches(window, tab)) {
result.push(tab);
}
}
}
runSafe(context, callback, result);
},
_execute: function(tabId, details, kind, callback) {
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
let mm = tab.linkedBrowser.messageManager;
let options = {js: [], css: []};
if (details.code) {
options[kind + 'Code'] = details.code;
}
if (details.file) {
options[kind].push(extension.baseURI.resolve(details.file));
}
if (details.allFrames) {
options.all_frames = details.allFrames;
}
if (details.matchAboutBlank) {
options.match_about_blank = details.matchAboutBlank;
}
if (details.runAt) {
options.run_at = details.runAt;
}
mm.sendAsyncMessage("Extension:Execute",
{extensionId: extension.id, options});
// TODO: Call the callback with the result (which is what???).
},
executeScript: function(...args) {
if (args.length == 1) {
self.tabs._execute(undefined, args[0], 'js', undefined);
} else {
self.tabs._execute(args[0], args[1], 'js', args[2]);
}
},
insertCss: function(tabId, details, callback) {
if (args.length == 1) {
self.tabs._execute(undefined, args[0], 'css', undefined);
} else {
self.tabs._execute(args[0], args[1], 'css', args[2]);
}
},
connect: function(tabId, connectInfo) {
let tab = TabManager.getTab(tabId);
let mm = tab.linkedBrowser.messageManager;
let name = connectInfo.name || "";
let recipient = {extensionId: extension.id};
if ("frameId" in connectInfo) {
recipient.frameId = connectInfo.frameId;
}
return context.messenger.connect(mm, name, recipient);
},
sendMessage: function(tabId, message, options, responseCallback) {
let tab = TabManager.getTab(tabId);
let mm = tab.linkedBrowser.messageManager;
let recipient = {extensionId: extension.id};
if (options && "frameId" in options) {
recipient.frameId = options.frameId;
}
return context.messenger.sendMessage(mm, message, recipient, responseCallback);
},
},
};
return self;
});

Просмотреть файл

@ -0,0 +1,324 @@
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
let {
EventManager,
} = ExtensionUtils;
// This file provides some useful code for the |tabs| and |windows|
// modules. All of the code is installed on |global|, which is a scope
// shared among the different ext-*.js scripts.
// Manages mapping between XUL tabs and extension tab IDs.
global.TabManager = {
_tabs: new WeakMap(),
_nextId: 1,
getId(tab) {
if (this._tabs.has(tab)) {
return this._tabs.get(tab);
}
let id = this._nextId++;
this._tabs.set(tab, id);
return id;
},
getBrowserId(browser) {
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
// Some non-browser windows have gBrowser but not
// getTabForBrowser!
if (gBrowser && gBrowser.getTabForBrowser) {
let tab = gBrowser.getTabForBrowser(browser);
if (tab) {
return this.getId(tab);
}
}
return -1;
},
getTab(tabId) {
// FIXME: Speed this up without leaking memory somehow.
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
if (!window.gBrowser) {
continue;
}
for (let tab of window.gBrowser.tabs) {
if (this.getId(tab) == tabId) {
return tab;
}
}
}
return null;
},
get activeTab() {
let window = WindowManager.topWindow;
if (window && window.gBrowser) {
return window.gBrowser.selectedTab;
}
return null;
},
getStatus(tab) {
return tab.getAttribute("busy") == "true" ? "loading" : "complete";
},
convert(extension, tab) {
let window = tab.ownerDocument.defaultView;
let windowActive = window == WindowManager.topWindow;
let result = {
id: this.getId(tab),
index: tab._tPos,
windowId: WindowManager.getId(window),
selected: tab.selected,
highlighted: tab.selected,
active: tab.selected,
pinned: tab.pinned,
status: this.getStatus(tab),
incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
width: tab.linkedBrowser.clientWidth,
height: tab.linkedBrowser.clientHeight,
};
if (extension.hasPermission("tabs")) {
result.url = tab.linkedBrowser.currentURI.spec;
if (tab.linkedBrowser.contentTitle) {
result.title = tab.linkedBrowser.contentTitle;
}
let icon = window.gBrowser.getIcon(tab);
if (icon) {
result.favIconUrl = icon;
}
}
return result;
},
getTabs(extension, window) {
if (!window.gBrowser) {
return [];
}
return [ for (tab of window.gBrowser.tabs) this.convert(extension, tab) ];
},
};
// Manages mapping between XUL windows and extension window IDs.
global.WindowManager = {
_windows: new WeakMap(),
_nextId: 0,
WINDOW_ID_NONE: -1,
WINDOW_ID_CURRENT: -2,
get topWindow() {
return Services.wm.getMostRecentWindow("navigator:browser");
},
windowType(window) {
// TODO: Make this work.
return "normal";
},
getId(window) {
if (this._windows.has(window)) {
return this._windows.get(window);
}
let id = this._nextId++;
this._windows.set(window, id);
return id;
},
getWindow(id) {
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
if (this.getId(window) == id) {
return window;
}
}
return null;
},
convert(extension, window, getInfo) {
let result = {
id: this.getId(window),
focused: window == WindowManager.topWindow,
top: window.screenY,
left: window.screenX,
width: window.outerWidth,
height: window.outerHeight,
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
// We fudge on these next two.
type: this.windowType(window),
state: window.fullScreen ? "fullscreen" : "normal",
};
if (getInfo && getInfo.populate) {
results.tabs = TabManager.getTabs(extension, window);
}
return result;
},
};
// Manages listeners for window opening and closing. A window is
// considered open when the "load" event fires on it. A window is
// closed when a "domwindowclosed" notification fires for it.
global.WindowListManager = {
_openListeners: new Set(),
_closeListeners: new Set(),
addOpenListener(listener, fireOnExisting = true) {
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
Services.ww.registerNotification(this);
}
this._openListeners.add(listener);
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
if (window.document.readyState != "complete") {
window.addEventListener("load", this);
} else if (fireOnExisting) {
listener(window);
}
}
},
removeOpenListener(listener) {
this._openListeners.delete(listener);
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
Services.ww.unregisterNotification(this);
}
},
addCloseListener(listener) {
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
Services.ww.registerNotification(this);
}
this._closeListeners.add(listener);
},
removeCloseListener(listener) {
this._closeListeners.delete(listener);
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
Services.ww.unregisterNotification(this);
}
},
handleEvent(event) {
let window = event.target.defaultView;
window.removeEventListener("load", this.loadListener);
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
return;
}
for (let listener of this._openListeners) {
listener(window);
}
},
queryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
observe(window, topic, data) {
if (topic == "domwindowclosed") {
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
return;
}
window.removeEventListener("load", this);
for (let listener of this._closeListeners) {
listener(window);
}
} else {
window.addEventListener("load", this);
}
},
};
// Provides a facility to listen for DOM events across all XUL windows.
global.AllWindowEvents = {
_listeners: new Map(),
// If |type| is a normal event type, invoke |listener| each time
// that event fires in any open window. If |type| is "progress", add
// a web progress listener that covers all open windows.
addListener(type, listener) {
if (type == "domwindowopened") {
return WindowListManager.addOpenListener(listener);
} else if (type == "domwindowclosed") {
return WindowListManager.addCloseListener(listener);
}
let needOpenListener = this._listeners.size == 0;
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
let list = this._listeners.get(type);
list.add(listener);
if (needOpenListener) {
WindowListManager.addOpenListener(this.openListener);
}
},
removeListener(type, listener) {
if (type == "domwindowopened") {
return WindowListManager.removeOpenListener(listener);
} else if (type == "domwindowclosed") {
return WindowListManager.removeCloseListener(listener);
}
let listeners = this._listeners.get(type);
listeners.delete(listener);
if (listeners.length == 0) {
this._listeners.delete(type);
if (this._listeners.size == 0) {
WindowListManager.removeOpenListener(this.openListener);
}
}
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let window = e.getNext();
if (type == "progress") {
window.gBrowser.removeTabsProgressListener(listener);
} else {
window.removeEventListener(type, listener);
}
}
},
// Runs whenever the "load" event fires for a new window.
openListener(window) {
for (let [eventType, listeners] of AllWindowEvents._listeners) {
for (let listener of listeners) {
if (eventType == "progress") {
window.gBrowser.addTabsProgressListener(listener);
} else {
window.addEventListener(eventType, listener);
}
}
}
},
};
// Subclass of EventManager where we just need to call
// add/removeEventListener on each XUL window.
global.WindowEventManager = function(context, name, event, listener)
{
EventManager.call(this, context, name, fire => {
let listener2 = (...args) => listener(fire, ...args);
AllWindowEvents.addListener(event, listener2);
return () => {
AllWindowEvents.removeListener(event, listener2);
}
});
}
WindowEventManager.prototype = Object.create(EventManager.prototype);

Просмотреть файл

@ -0,0 +1,151 @@
XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
"resource:///modules/NewTabURL.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
let {
EventManager,
ignoreEvent,
runSafe,
} = ExtensionUtils;
extensions.registerAPI((extension, context) => {
return {
windows: {
WINDOW_ID_CURRENT: WindowManager.WINDOW_ID_CURRENT,
WINDOW_ID_NONE: WindowManager.WINDOW_ID_NONE,
onCreated:
new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
fire(WindowManager.convert(extension, window));
}).api(),
onRemoved:
new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
fire(WindowManager.getId(window));
}).api(),
onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
// FIXME: This will send multiple messages for a single focus change.
let listener = event => {
let window = WindowManager.topWindow;
let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
fire(windowId);
};
AllWindowEvents.addListener("focus", listener);
AllWindowEvents.addListener("blur", listener);
return () => {
AllWindowEvents.removeListener("focus", listener);
AllWindowEvents.removeListener("blur", listener);
};
}).api(),
get: function(windowId, getInfo, callback) {
let window = WindowManager.getWindow(windowId);
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
},
getCurrent: function(getInfo, callback) {
let window = context.contentWindow;
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
},
getLastFocused: function(...args) {
let getInfo, callback;
if (args.length == 1) {
callback = args[0];
} else {
[getInfo, callback] = args;
}
let window = WindowManager.topWindow;
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
},
getAll: function(getAll, callback) {
let e = Services.wm.getEnumerator("navigator:browser");
let windows = [];
while (e.hasMoreElements()) {
let window = e.getNext();
windows.push(WindowManager.convert(extension, window, getInfo));
}
runSafe(context, callback, windows);
},
create: function(createData, callback) {
function mkstr(s) {
let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
result.data = s;
return result;
}
let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
if ("url" in createData) {
if (Array.isArray(createData.url)) {
let array = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
for (let url of createData.url) {
array.AppendElement(mkstr(url));
}
args.AppendElement(array);
} else {
args.AppendElement(mkstr(createData.url));
}
} else {
args.AppendElement(mkstr(NewTabURL.get()));
}
let extraFeatures = "";
if ("incognito" in createData) {
if (createData.incognito) {
extraFeatures += ",private";
} else {
extraFeatures += ",non-private";
}
}
let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
"chrome,dialog=no,all" + extraFeatures, args);
if ("left" in createData || "top" in createData) {
let left = "left" in createData ? createData.left : window.screenX;
let top = "top" in createData ? createData.top : window.screenY;
window.moveTo(left, top);
}
if ("width" in createData || "height" in createData) {
let width = "width" in createData ? createData.width : window.outerWidth;
let height = "height" in createData ? createData.height : window.outerHeight;
window.resizeTo(width, height);
}
// TODO: focused, type, state
window.addEventListener("load", function listener() {
window.removeEventListener("load", listener);
if (callback) {
runSafe(context, callback, WindowManager.convert(extension, window));
}
});
},
update: function(windowId, updateInfo, callback) {
let window = WindowManager.getWindow(windowId);
if (updateInfo.focused) {
Services.focus.activeWindow = window;
}
// TODO: All the other properties...
runSafe(context, callback, WindowManager.convert(extension, window));
},
remove: function(windowId, callback) {
let window = WindowManager.getWindow(windowId);
window.close();
let listener = () => {
AllWindowEvents.removeListener("domwindowclosed", listener);
if (callback) {
runSafe(context, callback);
}
};
AllWindowEvents.addListener("domwindowclosed", listener);
},
},
};
});

Просмотреть файл

@ -0,0 +1,19 @@
<?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/. -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.style-puzzle-piece {
fill: url('#gradient-linear-puzzle-piece');
}
</style>
<linearGradient id="gradient-linear-puzzle-piece" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#66cc52" stop-opacity="1"/>
<stop offset="100%" stop-color="#60bf4c" stop-opacity="1"/>
</linearGradient>
</defs>
<path class="style-puzzle-piece" d="M42,62c2.2,0,4-1.8,4-4l0-14.2c0,0,0.4-3.7,2.8-3.7c2.4,0,2.2,3.9,6.7,3.9c2.3,0,6.2-1.2,6.2-8.2 c0-7-3.9-7.9-6.2-7.9c-4.5,0-4.3,3.7-6.7,3.7c-2.4,0-2.8-3.8-2.8-3.8V22c0-2.2-1.8-4-4-4H31.5c0,0-3.4-0.6-3.4-3 c0-2.4,3.8-2.6,3.8-7.1c0-2.3-1.3-5.9-8.3-5.9s-8,3.6-8,5.9c0,4.5,3.4,4.7,3.4,7.1c0,2.4-3.4,3-3.4,3H6c-2.2,0-4,1.8-4,4l0,7.8 c0,0-0.4,6,4.4,6c3.1,0,3.2-4.1,7.3-4.1c2,0,4,1.9,4,6c0,4.2-2,6.3-4,6.3c-4,0-4.2-4.1-7.3-4.1c-4.8,0-4.4,5.8-4.4,5.8L2,58 c0,2.2,1.8,4,4,4H19c0,0,6.3,0.4,6.3-4.4c0-3.1-4-3.6-4-7.7c0-2,2.2-4.5,6.4-4.5c4.2,0,6.6,2.5,6.6,4.5c0,4-3.9,4.6-3.9,7.7 c0,4.9,6.3,4.4,6.3,4.4H42z"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Просмотреть файл

@ -0,0 +1,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/extension.svg (extension.svg)
content/browser/ext-utils.js (ext-utils.js)
content/browser/ext-contextMenus.js (ext-contextMenus.js)
content/browser/ext-browserAction.js (ext-browserAction.js)
content/browser/ext-tabs.js (ext-tabs.js)
content/browser/ext-windows.js (ext-windows.js)

Просмотреть файл

@ -1,7 +1,7 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/readinglist/sidebar.xhtml
content/browser/readinglist/sidebar.js
JAR_MANIFESTS += ['jar.mn']

Просмотреть файл

@ -0,0 +1,83 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import argparse
import json
import uuid
import sys
import os.path
parser = argparse.ArgumentParser(description='Create install.rdf from manifest.json')
parser.add_argument('--locale')
parser.add_argument('--profile')
parser.add_argument('--uuid')
parser.add_argument('dir')
args = parser.parse_args()
manifestFile = os.path.join(args.dir, 'manifest.json')
manifest = json.load(open(manifestFile))
locale = args.locale
if not locale:
locale = manifest.get('default_locale', 'en-US')
def process_locale(s):
if s.startswith('__MSG_') and s.endswith('__'):
tag = s[6:-2]
path = os.path.join(args.dir, '_locales', locale, 'messages.json')
data = json.load(open(path))
return data[tag]['message']
else:
return s
id = args.uuid
if not id:
id = '{' + str(uuid.uuid4()) + '}'
name = process_locale(manifest['name'])
desc = process_locale(manifest['description'])
version = manifest['version']
installFile = open(os.path.join(args.dir, 'install.rdf'), 'w')
print >>installFile, '<?xml version="1.0"?>'
print >>installFile, '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"'
print >>installFile, ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">'
print >>installFile
print >>installFile, ' <Description about="urn:mozilla:install-manifest">'
print >>installFile, ' <em:id>{}</em:id>'.format(id)
print >>installFile, ' <em:type>2</em:type>'
print >>installFile, ' <em:name>{}</em:name>'.format(name)
print >>installFile, ' <em:description>{}</em:description>'.format(desc)
print >>installFile, ' <em:version>{}</em:version>'.format(version)
print >>installFile, ' <em:bootstrap>true</em:bootstrap>'
print >>installFile, ' <em:targetApplication>'
print >>installFile, ' <Description>'
print >>installFile, ' <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>'
print >>installFile, ' <em:minVersion>4.0</em:minVersion>'
print >>installFile, ' <em:maxVersion>50.0</em:maxVersion>'
print >>installFile, ' </Description>'
print >>installFile, ' </em:targetApplication>'
print >>installFile, ' </Description>'
print >>installFile, '</RDF>'
installFile.close()
bootstrapPath = os.path.join(os.path.dirname(sys.argv[0]), 'bootstrap.js')
data = open(bootstrapPath).read()
boot = open(os.path.join(args.dir, 'bootstrap.js'), 'w')
boot.write(data)
boot.close()
if args.profile:
os.system('mkdir -p {}/extensions'.format(args.profile))
output = open(args.profile + '/extensions/' + id, 'w')
print >>output, os.path.realpath(args.dir)
output.close()
else:
dir = os.path.realpath(args.dir)
if dir[-1] == os.sep:
dir = dir[:-1]
os.system('cd "{}"; zip ../"{}".xpi -r *'.format(args.dir, os.path.basename(dir)))

Просмотреть файл

@ -22,6 +22,7 @@ content/js/panel.js
content/js/roomViews.js
content/js/feedbackViews.js
content/shared/js/textChatView.js
content/shared/js/linkifiedTextView.js
content/shared/js/views.js
standalone/content/js/fxOSMarketplace.js
standalone/content/js/standaloneRoomViews.js

Просмотреть файл

@ -39,6 +39,8 @@
<script type="text/javascript" src="loop/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
<script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>

Просмотреть файл

@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
"use strict";
/**
* Given a rawText property, renderer a version of that text with any
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
// ignored.
linkClickHandler: React.PropTypes.func,
// The text to be linkified.
rawText: React.PropTypes.string.isRequired,
// Should the links send a referrer? Defaults to false.
sendReferrer: React.PropTypes.bool,
// Should we suppress target="_blank" on the link? Defaults to false.
// Mostly for testing use.
suppressTarget: React.PropTypes.bool
},
mixins: [
React.addons.PureRenderMixin
],
_handleClickEvent: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
_generateLinkAttributes: function(href) {
var linkAttributes = {
href: href
};
if (this.props.linkClickHandler) {
linkAttributes.onClick = this._handleClickEvent;
// if this is specified, we short-circuit return to avoid unnecessarily
// creating target and rel attributes.
return linkAttributes;
}
if (!this.props.suppressTarget) {
linkAttributes.target = "_blank";
}
if (!this.props.sendReferrer) {
linkAttributes.rel = "noreferrer";
}
return linkAttributes;
},
/** a
* Parse the given string into an array of strings and React <a> elements
* in the order in which they should be rendered (i.e. FIFO).
*
* @param {String} s the raw string to be parsed
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function(s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
while (result) {
// If there's text preceding the first link, push it onto the array
// and update the string pointer.
if (result.index) {
elements.push(s.substr(0, result.index));
s = s.substr(result.index);
}
// Push the first link itself, and advance the string pointer again.
elements.push(
React.createElement("a", React.__spread({}, this._generateLinkAttributes(result[0]) ,
{key: reactElementsCounter++}),
result[0]
)
);
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
}
if (s) {
elements.push(s);
}
return elements;
},
render: function () {
return ( React.createElement("p", null, this.parseStringToElements(this.props.rawText) ) );
}
});
return LinkifiedTextView;
})(navigator.mozL10n || document.mozL10n);

Просмотреть файл

@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
"use strict";
/**
* Given a rawText property, renderer a version of that text with any
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
// ignored.
linkClickHandler: React.PropTypes.func,
// The text to be linkified.
rawText: React.PropTypes.string.isRequired,
// Should the links send a referrer? Defaults to false.
sendReferrer: React.PropTypes.bool,
// Should we suppress target="_blank" on the link? Defaults to false.
// Mostly for testing use.
suppressTarget: React.PropTypes.bool
},
mixins: [
React.addons.PureRenderMixin
],
_handleClickEvent: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
_generateLinkAttributes: function(href) {
var linkAttributes = {
href: href
};
if (this.props.linkClickHandler) {
linkAttributes.onClick = this._handleClickEvent;
// if this is specified, we short-circuit return to avoid unnecessarily
// creating target and rel attributes.
return linkAttributes;
}
if (!this.props.suppressTarget) {
linkAttributes.target = "_blank";
}
if (!this.props.sendReferrer) {
linkAttributes.rel = "noreferrer";
}
return linkAttributes;
},
/** a
* Parse the given string into an array of strings and React <a> elements
* in the order in which they should be rendered (i.e. FIFO).
*
* @param {String} s the raw string to be parsed
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function(s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
while (result) {
// If there's text preceding the first link, push it onto the array
// and update the string pointer.
if (result.index) {
elements.push(s.substr(0, result.index));
s = s.substr(result.index);
}
// Push the first link itself, and advance the string pointer again.
elements.push(
<a { ...this._generateLinkAttributes(result[0]) }
key={reactElementsCounter++}>
{result[0]}
</a>
);
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
}
if (s) {
elements.push(s);
}
return elements;
},
render: function () {
return ( <p>{ this.parseStringToElements(this.props.rawText) }</p> );
}
});
return LinkifiedTextView;
})(navigator.mozL10n || document.mozL10n);

Просмотреть файл

@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
var optionalProps = {};
if (navigator.mozLoop) {
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
}
return (
React.createElement("div", {className: classes},
React.createElement("p", null, this.props.message),
React.createElement(sharedViews.LinkifiedTextView, React.__spread({}, optionalProps,
{rawText: this.props.message})),
React.createElement("span", {className: "text-chat-arrow"}),
this.props.showTimestamp ? this._renderTimestamp() : null
)

Просмотреть файл

@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
var optionalProps = {};
if (navigator.mozLoop) {
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
}
return (
<div className={classes}>
<p>{this.props.message}</p>
<sharedViews.LinkifiedTextView {...optionalProps}
rawText={this.props.message} />
<span className="text-chat-arrow" />
{this.props.showTimestamp ? this._renderTimestamp() : null}
</div>

Просмотреть файл

@ -0,0 +1,72 @@
// This is derived from Diego Perini's code,
// currently available at https://gist.github.com/dperini/729294
// Regular Expression for URL validation
//
// Original Author: Diego Perini
// License: MIT
//
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.urlRegExps = (function() {
"use strict";
// Some https://wiki.mozilla.org/Loop/Development/RegExpDebugging for tools
// if you need to debug changes to this:
var fullUrlMatch = new RegExp(
// Protocol identifier.
"(?:(?:https?|ftp)://)" +
// User:pass authentication.
"((?:\\S+(?::\\S*)?@)?" +
"(?:" +
// IP address dotted notation octets:
// excludes loopback network 0.0.0.0,
// excludes reserved space >= 224.0.0.0,
// excludes network & broadcast addresses,
// (first & last IP address of each class).
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// Host name.
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
// Domain name.
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
// TLD identifier.
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
// Port number.
"(?::\\d{2,5})?" +
// Resource path.
"(?:[/?#]\\S*)?)", "i");
return {
fullUrlMatch: fullUrlMatch
};
})();

Просмотреть файл

@ -86,8 +86,10 @@ browser.jar:
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
content/browser/loop/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)

Просмотреть файл

@ -144,9 +144,10 @@
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="shared/js/textChatStore.js"></script>
<script type="text/javascript" src="shared/js/textChatView.js"></script>
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>

Просмотреть файл

@ -34,6 +34,8 @@ module.exports = function(config) {
"content/shared/js/views.js",
"content/shared/js/textChatStore.js",
"content/shared/js/textChatView.js",
"content/shared/js/urlRegExps.js",
"content/shared/js/linkifiedTextView.js",
"standalone/content/js/multiplexGum.js",
"standalone/content/js/standaloneAppStore.js",
"standalone/content/js/standaloneClient.js",

Просмотреть файл

@ -63,6 +63,8 @@
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/urlRegExps.js"></script>
<script src="../../content/shared/js/linkifiedTextView.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
@ -80,6 +82,8 @@
<script src="store_test.js"></script>
<script src="textChatStore_test.js"></script>
<script src="textChatView_test.js"></script>
<script src="linkifiedTextView_test.js"></script>
<script>
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {

Просмотреть файл

@ -0,0 +1,378 @@
/*
* Many of these tests are ported from Autolinker.js:
*
* https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
*
* which is released under the MIT license. Thanks to Greg Jacobs for his hard
* work there.
*
* The MIT License (MIT)
* Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("loop.shared.views.LinkifiedTextView", function () {
"use strict";
var expect = chai.expect;
var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
var TestUtils = React.addons.TestUtils;
describe("LinkifiedTextView", function () {
function renderToMarkup(string, extraProps) {
return React.renderToStaticMarkup(
React.createElement(
LinkifiedTextView,
_.extend({rawText: string}, extraProps)));
}
describe("#render", function() {
function testRender(testData) {
it(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{suppressTarget: true, sendReferrer: true});
expect(markup).to.equal(testData.markup);
});
}
function testSkip(testData) {
it.skip(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{suppressTarget: true, sendReferrer: true});
expect(markup).to.equal(testData.markup);
});
}
describe("this.props.suppressTarget", function() {
it("should make links w/o a target attr if suppressTarget is true",
function() {
var markup = renderToMarkup("http://example.com", {suppressTarget: true});
expect(markup).to.equal(
'<p><a href="http://example.com" rel="noreferrer">http://example.com</a></p>');
});
it("should make links with target=_blank if suppressTarget is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
});
});
describe("this.props.sendReferrer", function() {
it("should make links w/o rel=noreferrer if sendReferrer is true",
function() {
var markup = renderToMarkup("http://example.com", {sendReferrer: true});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank">http://example.com</a></p>');
});
it("should make links with rel=noreferrer if sendReferrer is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
});
});
describe("this.props.linkClickHandler", function () {
function mountTestComponent(string, extraProps) {
return TestUtils.renderIntoDocument(
React.createElement(
LinkifiedTextView,
_.extend({rawText: string}, extraProps)));
}
it("should be called when a generated link is clicked", function () {
var fakeUrl = "http://example.com";
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(fakeUrl, {linkClickHandler: linkClickHandler});
TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
sinon.assert.calledOnce(linkClickHandler);
});
it("should cause sendReferrer and suppressTarget props to be ignored",
function() {
var fakeUrl = "http://example.com";
var linkClickHandler = function() {};
var markup = renderToMarkup("http://example.com", {
linkClickHandler: linkClickHandler,
sendReferrer: false,
suppressTarget: false
});
expect(markup).to.equal(
'<p><a href="http://example.com">http://example.com</a></p>');
});
describe("#_handleClickEvent", function () {
var fakeEvent;
var fakeUrl = "http://example.com";
beforeEach(function() {
fakeEvent = {
currentTarget: { href: fakeUrl },
preventDefault: sinon.stub(),
stopPropagation: sinon.stub()
};
});
it("should call preventDefault on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.preventDefault);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
it("should call stopPropagation on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.stopPropagation);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(linkClickHandler);
sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
});
});
});
// Note that these are really integration tests with the parser and React.
// Since we're depending on that integration to provide us with security
// against various injection problems, it feels fairly important. That
// said, these tests are not terribly robust in the face of markup changes
// in the code, and over time, some of them may want to be pushed down
// to only be unit tests against the parser or against
// parseStringToElements. We may also want both unit and integration
// testing for some subset of these.
var tests = [
{
desc: "should only add a container to a string with no URLs",
rawText: "This is a test.",
markup: "<p>This is a test.</p>"
},
{
desc: "should linkify a string containing only a URL",
rawText: "http://example.com/",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should linkify a URL with text preceding it",
rawText: "This is a link to http://example.com",
markup: '<p>This is a link to <a href="http://example.com">http://example.com</a></p>'
},
{
desc: "should linkify a URL with text before and after",
rawText: "Look at http://example.com near the bottom",
markup: '<p>Look at <a href="http://example.com">http://example.com</a> near the bottom</p>'
},
{
desc: "should linkify an http URL",
rawText: "This is an http://example.com test",
markup: '<p>This is an <a href="http://example.com">http://example.com</a> test</p>'
},
{
desc: "should linkify an https URL",
rawText: "This is an https://example.com test",
markup: '<p>This is an <a href="https://example.com">https://example.com</a> test</p>'
},
{
desc: "should not linkify a data URL",
rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
},
{
desc: "should linkify URLs with a port number",
rawText: "Joe went to http://example.com:8000 today.",
markup: '<p>Joe went to <a href="http://example.com:8000">http://example.com:8000</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a trailing slash",
rawText: "Joe went to http://example.com:8000/ today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a path",
rawText: "Joe went to http://example.com:8000/mysite/page today.",
markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a query string",
rawText: "Joe went to http://example.com:8000?page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000?page=index">http://example.com:8000?page=index</a> today.</p>'
},
{
desc: "should linkify a URL with a port number and a hash string",
rawText: "Joe went to http://example.com:8000#page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000#page=index">http://example.com:8000#page=index</a> today.</p>'
},
{
desc: "should NOT include preceding ':' intros without a space",
rawText: "the link:http://example.com/",
markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should NOT autolink URLs with 'javascript:' URI scheme",
rawText: "do not link javascript:window.alert('hi') please",
markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
rawText: "do not link JavAscriPt:window.alert('hi') please",
markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
rawText: "do not link vbscript:window.alert('hi') please",
markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
rawText: "do not link vBsCriPt:window.alert('hi') please",
markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink a string in the form of 'version:1.0'",
rawText: "version:1.0",
markup: "<p>version:1.0</p>"
},
{
desc: "should linkify an ftp URL",
rawText: "This is an ftp://example.com test",
markup: '<p>This is an <a href="ftp://example.com">ftp://example.com</a> test</p>'
},
// We don't want to include trailing dots in URLs, even though those
// are valid DNS names, as that should match user intent more of the
// time, as well as avoid this stuff:
//
// http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
//
{
desc: "should linkify 'http://example.com.', w/o a trailing dot",
rawText: "Joe went to http://example.com.",
markup: '<p>Joe went to <a href="http://example.com">http://example.com</a>.</p>'
},
// XXX several more tests like this we could port from Autolinkify.js
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should exclude invalid chars after domain part",
rawText: "Joe went to http://www.example.com's today",
markup: '<p>Joe went to <a href="http://www.example.com">http://www.example.com</a>&#x27;s today</p>'
},
{
desc: "should not linkify protocol-relative URLs",
rawText: "//C/Programs",
markup: "<p>//C/Programs</p>"
},
// do a few tests to convince ourselves that, when our code is handled
// HTML in the input box, the integration of our code with React should
// cause that to rendered to appear as HTML source code, rather than
// getting injected into our real HTML DOM
{
desc: "should linkify simple HTML include an href properly escaped",
rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com">http://www.example.com</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>'
},
{
desc: "should linkify HTML with nested tags and resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com">http://example.com</a>&quot; /&gt;&lt;/a&gt;</p>'
}
];
var skippedTests = [
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should link localhost URLs with an allowed URL scheme",
rawText: "Joe went to http://localhost today",
markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
},
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should not include a ? if at the end of a URL",
rawText: "Did Joe go to http://example.com?",
markup: '<p>Did Joe go to <a href="http://example.com">http://example.com</a>?</p>'
},
{
desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
rawText: "check out http://example.com/monkey...",
markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
},
// another variant of eating too many trailing characters, it includes
// the trailing ", which it shouldn't. Makes links inside pasted HTML
// source be slightly broken. Not key for our target users, I suspect,
// but still...
{
desc: "should linkify HTML with nested tags and a resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>'
},
// XXX handle domains without schemes (bug 1186245)
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should linkify a.museum (known TLD), but not abc.qqqq",
rawText: "a.museum should be linked, but abc.qqqq should not",
markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
},
{
desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
rawText: "example.xyz should be linked, example.etc should not",
rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
}
];
tests.forEach(testRender);
// XXX Over time, we'll want to make these pass..
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
skippedTests.forEach(testSkip);
});
});
});

Просмотреть файл

@ -250,6 +250,21 @@ describe("loop.shared.views.TextChatView", function () {
expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
});
// note that this is really an integration test to be sure that we don't
// inadvertently regress using LinkifiedTextView.
it("should linkify a URL starting with http", function (){
view = mountTestComponent({
showTimestamp: true,
timestamp: "2015-06-23T22:48:39.738Z",
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Check out http://example.com and see what you think..."
});
var node = view.getDOMNode();
expect(node.querySelector("a")).to.not.eql(null);
});
});
describe("TextChatView", function() {

Просмотреть файл

@ -58,6 +58,8 @@
<script src="../content/shared/js/textChatStore.js"></script>
<script src="../content/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatView.js"></script>
<script src="../content/shared/js/urlRegExps.js"></script>
<script src="../content/shared/js/linkifiedTextView.js"></script>
<script src="../content/js/roomStore.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>

Просмотреть файл

@ -86,9 +86,11 @@
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
sendTextChatMessage: function(actionData) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: actionData.message,
receivedTimestamp: actionData.sentTimestamp
}));
}
};
@ -394,16 +396,17 @@
message: "Rheet!",
sentTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hi there",
receivedTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hello",
receivedTimestamp: "2015-06-23T23:24:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Check out this menu from DNA Pizza:" +
@ -411,12 +414,6 @@
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "That avocado monkey-brains pie sounds tasty!",
@ -427,10 +424,10 @@
message: "What time should we meet?",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
message: "8:00 PM",
receivedTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({

Просмотреть файл

@ -86,9 +86,11 @@
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
sendTextChatMessage: function(actionData) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: actionData.message,
receivedTimestamp: actionData.sentTimestamp
}));
}
};
@ -394,16 +396,17 @@
message: "Rheet!",
sentTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hi there",
receivedTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hello",
receivedTimestamp: "2015-06-23T23:24:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Check out this menu from DNA Pizza:" +
@ -411,12 +414,6 @@
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "That avocado monkey-brains pie sounds tasty!",
@ -427,10 +424,10 @@
message: "What time should we meet?",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
message: "8:00 PM",
receivedTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({

Просмотреть файл

@ -9,6 +9,7 @@ DIRS += [
'customizableui',
'dirprovider',
'downloads',
'extensions',
'feeds',
'loop',
'migration',
@ -16,7 +17,6 @@ DIRS += [
'pocket',
'preferences',
'privatebrowsing',
'readinglist',
'search',
'sessionstore',
'shell',

Просмотреть файл

@ -169,6 +169,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonWatcher",
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
"resource://gre/modules/LightweightThemeManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
@ -601,6 +604,12 @@ BrowserGlue.prototype = {
os.addObserver(this, "xpi-signature-changed", false);
os.addObserver(this, "autocomplete-did-enter-text", false);
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
this._flashHangCount = 0;
},

Просмотреть файл

@ -18,9 +18,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
const PAGE_NO_ACCOUNT = 0;
const PAGE_HAS_ACCOUNT = 1;
const PAGE_NEEDS_UPDATE = 2;
const PAGE_PLEASE_WAIT = 3;
const FXA_PAGE_LOGGED_OUT = 4;
const FXA_PAGE_LOGGED_IN = 5;
const FXA_PAGE_LOGGED_OUT = 3;
const FXA_PAGE_LOGGED_IN = 4;
// Indexes into the "login status" deck.
// We are in a successful verified state - everything should work!
@ -68,7 +67,7 @@ let gSyncPane = {
// it may take some time before we can determine what provider to use
// and the state of that provider, so show the "please wait" page.
this.page = PAGE_PLEASE_WAIT;
this._showLoadPage(xps);
let onUnload = function () {
window.removeEventListener("unload", onUnload, false);
@ -89,6 +88,30 @@ let gSyncPane = {
xps.ensureLoaded();
},
_showLoadPage: function (xps) {
let username;
try {
username = Services.prefs.getCharPref("services.sync.username");
} catch (e) {}
if (!username) {
this.page = FXA_PAGE_LOGGED_OUT;
} else if (xps.fxAccountsEnabled) {
// Use cached values while we wait for the up-to-date values
let cachedComputerName;
try {
cachedComputerName = Services.prefs.getCharPref("services.sync.client.name");
}
catch (e) {
cachedComputerName = "";
}
document.getElementById("fxaEmailAddress1").textContent = username;
document.getElementById("fxaSyncComputerName").value = cachedComputerName;
this.page = FXA_PAGE_LOGGED_IN;
} else { // Old Sync
this.page = PAGE_HAS_ACCOUNT;
}
},
_init: function () {
let topics = ["weave:service:login:error",
"weave:service:login:finish",
@ -295,19 +318,13 @@ let gSyncPane = {
fxaEmailAddress1Label.hidden = false;
displayNameLabel.hidden = true;
// unhide the reading-list engine if readinglist is enabled (note we do
// it here as it must remain disabled for legacy sync users)
if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
document.getElementById("readinglist-engine").removeAttribute("hidden");
}
let profileInfoEnabled;
try {
profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled");
} catch (ex) {}
// determine the fxa status...
this.page = PAGE_PLEASE_WAIT;
this._showLoadPage(service);
fxAccounts.getSignedInUser().then(data => {
if (!data) {

Просмотреть файл

@ -24,10 +24,6 @@
<preference id="engine.passwords"
name="services.sync.engine.passwords"
type="bool"/>
<!-- non Sync-Engine engines -->
<preference id="engine.readinglist"
name="readinglist.scheduler.enabled"
type="bool"/>
</preferences>
<script type="application/javascript"
@ -204,12 +200,6 @@
</vbox>
<!-- These panels are for the Firefox Accounts identity provider -->
<vbox id="fxaDeterminingStatus" align="center">
<spacer flex="1"/>
<label>&determiningAcctStatus.label;</label>
<spacer flex="1"/>
</vbox>
<vbox id="noFxaAccount" align="start">
<label>&welcome.description;</label>
<label id="noFxaSignUp" class="text-link">
@ -306,11 +296,6 @@
<checkbox label="&engine.history.label;"
accesskey="&engine.history.accesskey;"
preference="engine.history"/>
<checkbox id="readinglist-engine"
label="&engine.readinglist.label;"
accesskey="&engine.readinglist.accesskey;"
preference="engine.readinglist"
hidden="true"/>
<checkbox label="&engine.addons.label;"
accesskey="&engine.addons.accesskey;"
preference="engine.addons"/>

Просмотреть файл

@ -156,11 +156,6 @@ let gSyncPane = {
// service.fxAccountsEnabled is false iff sync is already configured for
// the legacy provider.
if (service.fxAccountsEnabled) {
// unhide the reading-list engine if readinglist is enabled (note we do
// it here as it must remain disabled for legacy sync users)
if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
document.getElementById("readinglist-engine").removeAttribute("hidden");
}
// determine the fxa status...
this.page = PAGE_PLEASE_WAIT;
fxAccounts.getSignedInUser().then(data => {

Просмотреть файл

@ -28,8 +28,6 @@
<preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/>
<preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/>
<preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
<!-- non Sync-Engine engines -->
<preference id="engine.readinglist" name="readinglist.scheduler.enabled" type="bool"/>
</preferences>
@ -301,12 +299,6 @@
accesskey="&engine.history.accesskey;"
onsynctopreference="gSyncPane.onPreferenceChanged(this);"
preference="engine.history"/>
<!-- onpreferencechanged not needed for the readinglist engine -->
<checkbox id="readinglist-engine"
label="&engine.readinglist.label;"
accesskey="&engine.readinglist.accesskey;"
preference="engine.readinglist"
hidden="true"/>
<checkbox label="&engine.addons.label;"
accesskey="&engine.addons.accesskey;"
onsynctopreference="gSyncPane.onPreferenceChanged();"

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,466 +0,0 @@
/* 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";
this.EXPORTED_SYMBOLS = [
"SQLiteStore",
];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
/**
* A SQLite Reading List store backed by a database on disk. The database is
* created if it doesn't exist.
*
* @param pathRelativeToProfileDir The path of the database file relative to
* the profile directory.
*/
this.SQLiteStore = function SQLiteStore(pathRelativeToProfileDir) {
this.pathRelativeToProfileDir = pathRelativeToProfileDir;
};
this.SQLiteStore.prototype = {
/**
* Yields the number of items in the store that match the given options.
*
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return Promise<number> The number of matching items in the store.
* Rejected with an Error on error.
*/
count: Task.async(function* (userOptsList=[], controlOpts={}) {
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
let count = 0;
let conn = yield this._connectionPromise;
yield conn.executeCached(`
SELECT COUNT(*) AS count FROM items ${sql};
`, args, row => count = row.getResultByName("count"));
return count;
}),
/**
* Enumerates the items in the store that match the given options.
*
* @param callback Called for each item in the enumeration. It's passed a
* single object, an item.
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return Promise<null> Resolved when the enumeration completes. Rejected
* with an Error on error.
*/
forEachItem: Task.async(function* (callback, userOptsList=[], controlOpts={}) {
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
let colNames = ReadingList.ItemRecordProperties;
let conn = yield this._connectionPromise;
yield conn.executeCached(`
SELECT ${colNames} FROM items ${sql};
`, args, row => callback(itemFromRow(row)));
}),
/**
* Adds an item to the store that isn't already present. See
* ReadingList.prototype.addItem.
*
* @param items A simple object representing an item.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
addItem: Task.async(function* (item) {
let colNames = [];
let paramNames = [];
for (let propName in item) {
colNames.push(propName);
paramNames.push(`:${propName}`);
}
let conn = yield this._connectionPromise;
try {
yield conn.executeCached(`
INSERT INTO items (${colNames}) VALUES (${paramNames});
`, item);
}
catch (err) {
throwExistsError(err);
}
}),
/**
* Updates the properties of an item that's already present in the store. See
* ReadingList.prototype.updateItem.
*
* @param item The item to update. It must have a `url`.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
updateItem: Task.async(function* (item) {
yield this._updateItem(item, "url");
}),
/**
* Same as updateItem, but the item is keyed off of its `guid` instead of its
* `url`.
*
* @param item The item to update. It must have a `guid`.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
updateItemByGUID: Task.async(function* (item) {
yield this._updateItem(item, "guid");
}),
/**
* Deletes an item from the store by its URL.
*
* @param url The URL string of the item to delete.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
deleteItemByURL: Task.async(function* (url) {
let conn = yield this._connectionPromise;
yield conn.executeCached(`
DELETE FROM items WHERE url = :url;
`, { url: url });
}),
/**
* Deletes an item from the store by its GUID.
*
* @param guid The GUID string of the item to delete.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
deleteItemByGUID: Task.async(function* (guid) {
let conn = yield this._connectionPromise;
yield conn.executeCached(`
DELETE FROM items WHERE guid = :guid;
`, { guid: guid });
}),
/**
* Call this when you're done with the store. Don't use it afterward.
*/
destroy() {
if (!this._destroyPromise) {
this._destroyPromise = Task.spawn(function* () {
let conn = yield this._connectionPromise;
yield conn.close();
this.__connectionPromise = Promise.reject("Store destroyed");
}.bind(this));
}
return this._destroyPromise;
},
/**
* Promise<Sqlite.OpenedConnection>
*/
get _connectionPromise() {
if (!this.__connectionPromise) {
this.__connectionPromise = this._createConnection();
}
return this.__connectionPromise;
},
/**
* Creates the database connection.
*
* @return Promise<Sqlite.OpenedConnection>
*/
_createConnection: Task.async(function* () {
let conn = yield Sqlite.openConnection({
path: this.pathRelativeToProfileDir,
sharedMemoryCache: false,
});
Sqlite.shutdown.addBlocker("readinglist/SQLiteStore: Destroy",
this.destroy.bind(this));
yield conn.execute(`
PRAGMA locking_mode = EXCLUSIVE;
`);
yield this._checkSchema(conn);
return conn;
}),
/**
* Updates the properties of an item that's already present in the store. See
* ReadingList.prototype.updateItem.
*
* @param item The item to update. It must have the property named by
* keyProp.
* @param keyProp The item is keyed off of this property.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
_updateItem: Task.async(function* (item, keyProp) {
let assignments = [];
for (let propName in item) {
assignments.push(`${propName} = :${propName}`);
}
let conn = yield this._connectionPromise;
if (!item[keyProp]) {
throw new ReadingList.Error.Error("Item must have " + keyProp);
}
try {
yield conn.executeCached(`
UPDATE items SET ${assignments} WHERE ${keyProp} = :${keyProp};
`, item);
}
catch (err) {
throwExistsError(err);
}
}),
// The current schema version.
_schemaVersion: 1,
_checkSchema: Task.async(function* (conn) {
let version = parseInt(yield conn.getSchemaVersion());
for (; version < this._schemaVersion; version++) {
let meth = `_migrateSchema${version}To${version + 1}`;
yield this[meth](conn);
}
yield conn.setSchemaVersion(this._schemaVersion);
}),
_migrateSchema0To1: Task.async(function* (conn) {
yield conn.execute(`
PRAGMA journal_mode = wal;
`);
// 524288 bytes = 512 KiB
yield conn.execute(`
PRAGMA journal_size_limit = 524288;
`);
// Not important, but FYI: The order that these columns are listed in
// follows the order that the server doc lists the fields in the article
// data model, more or less:
// http://readinglist.readthedocs.org/en/latest/model.html
yield conn.execute(`
CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guid TEXT UNIQUE,
serverLastModified INTEGER,
url TEXT UNIQUE,
preview TEXT,
title TEXT,
resolvedURL TEXT UNIQUE,
resolvedTitle TEXT,
excerpt TEXT,
archived BOOLEAN,
deleted BOOLEAN,
favorite BOOLEAN,
isArticle BOOLEAN,
wordCount INTEGER,
unread BOOLEAN,
addedBy TEXT,
addedOn INTEGER,
storedOn INTEGER,
markedReadBy TEXT,
markedReadOn INTEGER,
readPosition INTEGER,
syncStatus INTEGER
);
`);
yield conn.execute(`
CREATE INDEX items_addedOn ON items (addedOn);
`);
yield conn.execute(`
CREATE INDEX items_unread ON items (unread);
`);
}),
};
/**
* Returns a simple object whose properties are the
* ReadingList.ItemRecordProperties lifted from the given row.
*
* @param row A mozIStorageRow.
* @return The item.
*/
function itemFromRow(row) {
let item = {};
for (let name of ReadingList.ItemRecordProperties) {
item[name] = row.getResultByName(name);
}
return item;
}
/**
* If the given Error indicates that a unique constraint failed, then wraps that
* error in a ReadingList.Error.Exists and throws it. Otherwise throws the
* given error.
*
* @param err An Error object.
*/
function throwExistsError(err) {
let match =
/UNIQUE constraint failed: items\.([a-zA-Z0-9_]+)/.exec(err.message);
if (match) {
let newErr = new ReadingList.Error.Exists(
"An item with the following property already exists: " + match[1]
);
newErr.originalError = err;
err = newErr;
}
throw err;
}
/**
* Returns the back part of a SELECT statement generated from the given list of
* options.
*
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return An array [sql, args]. sql is a string of SQL. args is an object
* that contains arguments for all the parameters in sql.
*/
function sqlWhereFromOptions(userOptsList, controlOpts) {
// We modify the options objects in userOptsList, which were passed in by the
// store client, so clone them first.
userOptsList = Cu.cloneInto(userOptsList, {}, { cloneFunctions: false });
let sort;
let sortDir;
let limit;
let offset;
for (let opts of userOptsList) {
if ("sort" in opts) {
sort = opts.sort;
delete opts.sort;
}
if ("descending" in opts) {
if (opts.descending) {
sortDir = "DESC";
}
delete opts.descending;
}
if ("limit" in opts) {
limit = opts.limit;
delete opts.limit;
}
if ("offset" in opts) {
offset = opts.offset;
delete opts.offset;
}
}
let fragments = [];
if (sort) {
sortDir = sortDir || "ASC";
fragments.push(`ORDER BY ${sort} ${sortDir}`);
}
if (limit) {
fragments.push(`LIMIT ${limit}`);
if (offset) {
fragments.push(`OFFSET ${offset}`);
}
}
let args = {};
let mainExprs = [];
let controlSQLExpr = sqlExpressionFromOptions([controlOpts], args);
if (controlSQLExpr) {
mainExprs.push(`(${controlSQLExpr})`);
}
let userSQLExpr = sqlExpressionFromOptions(userOptsList, args);
if (userSQLExpr) {
mainExprs.push(`(${userSQLExpr})`);
}
if (mainExprs.length) {
let conjunction = mainExprs.join(" AND ");
fragments.unshift(`WHERE ${conjunction}`);
}
let sql = fragments.join(" ");
return [sql, args];
}
/**
* Returns a SQL expression generated from the given options list. Each options
* object in the list generates a subexpression, and all the subexpressions are
* OR'ed together to produce the final top-level expression. (e.g., an optsList
* with three options objects would generate an expression like "(guid = :guid
* OR (title = :title AND unread = :unread) OR resolvedURL = :resolvedURL)".)
*
* All the properties of the options objects are assumed to refer to columns in
* the database. If they don't, your SQL query will fail.
*
* @param optsList See Options Objects in ReadingList.jsm.
* @param args An object that will hold the SQL parameters. It will be
* modified.
* @return A string of SQL. Also, args will contain arguments for all the
* parameters in the SQL.
*/
function sqlExpressionFromOptions(optsList, args) {
let disjunctions = [];
for (let opts of optsList) {
let conjunctions = [];
for (let key in opts) {
if (Array.isArray(opts[key])) {
// Convert arrays to IN expressions. e.g., { guid: ['a', 'b', 'c'] }
// becomes "guid IN (:guid, :guid_1, :guid_2)". The guid_i arguments
// are added to opts.
let array = opts[key];
let params = [];
for (let i = 0; i < array.length; i++) {
let paramName = uniqueParamName(args, key);
params.push(`:${paramName}`);
args[paramName] = array[i];
}
conjunctions.push(`${key} IN (${params})`);
}
else {
let paramName = uniqueParamName(args, key);
conjunctions.push(`${key} = :${paramName}`);
args[paramName] = opts[key];
}
}
let conjunction = conjunctions.join(" AND ");
if (conjunction) {
disjunctions.push(`(${conjunction})`);
}
}
let disjunction = disjunctions.join(" OR ");
return disjunction;
}
/**
* Returns a version of the given name such that it doesn't conflict with the
* name of any property in args. e.g., if name is "foo" but args already has
* properties named "foo", "foo1", and "foo2", then "foo3" is returned.
*
* @param args An object.
* @param name The name you want to use.
* @return A unique version of the given name.
*/
function uniqueParamName(args, name) {
if (name in args) {
for (let i = 1; ; i++) {
let newName = `${name}_${i}`;
if (!(newName in args)) {
return newName;
}
}
}
return name;
}

Просмотреть файл

@ -1,409 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict;"
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import('resource://gre/modules/Task.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'LogManager',
'resource://services-common/logmanager.js');
XPCOMUtils.defineLazyModuleGetter(this, 'Log',
'resource://gre/modules/Log.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Preferences',
'resource://gre/modules/Preferences.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout',
'resource://gre/modules/Timer.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout',
'resource://gre/modules/Timer.jsm');
// The main readinglist module.
XPCOMUtils.defineLazyModuleGetter(this, 'ReadingList',
'resource:///modules/readinglist/ReadingList.jsm');
// The "engine"
XPCOMUtils.defineLazyModuleGetter(this, 'Sync',
'resource:///modules/readinglist/Sync.jsm');
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
XPCOMUtils.defineLazyGetter(this, "fxAccountsCommon", function() {
let namespace = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", namespace);
return namespace;
});
this.EXPORTED_SYMBOLS = ["ReadingListScheduler"];
// A list of "external" observer topics that may cause us to change when we
// sync.
const OBSERVERS = [
// We don't sync when offline and restart when online.
"network:offline-status-changed",
// FxA notifications also cause us to check if we should sync.
"fxaccounts:onverified",
// some notifications the engine might send if we have been requested to backoff.
"readinglist:backoff-requested",
// request to sync now
"readinglist:user-sync",
];
let prefs = new Preferences("readinglist.scheduler.");
// A helper to manage our interval values.
let intervals = {
// Getters for our intervals.
_fixupIntervalPref(prefName, def) {
// All pref values are seconds, but we return ms.
return prefs.get(prefName, def) * 1000;
},
// How long after startup do we do an initial sync?
get initial() this._fixupIntervalPref("initial", 10), // 10 seconds.
// Every interval after the first.
get schedule() this._fixupIntervalPref("schedule", 2 * 60 * 60), // 2 hours
// Initial retry after an error (exponentially backed-off to .schedule)
get retry() this._fixupIntervalPref("retry", 2 * 60), // 2 mins
};
// This is the implementation, but it's not exposed directly.
function InternalScheduler(readingList = null) {
// oh, I don't know what logs yet - let's guess!
let logs = [
"browserwindow.syncui",
"FirefoxAccounts",
"readinglist.api",
"readinglist.scheduler",
"readinglist.serverclient",
"readinglist.sync",
];
this._logManager = new LogManager("readinglist.", logs, "readinglist");
this.log = Log.repository.getLogger("readinglist.scheduler");
this.log.info("readinglist scheduler created.")
this.state = this.STATE_OK;
this.readingList = readingList || ReadingList; // hook point for tests.
// don't this.init() here, but instead at the module level - tests want to
// add hooks before it is called.
}
InternalScheduler.prototype = {
// When the next scheduled sync should happen. If we can sync, there will
// be a timer set to fire then. If we can't sync there will not be a timer,
// but it will be set to fire then as soon as we can.
_nextScheduledSync: null,
// The time when the most-recent "backoff request" expires - we will never
// schedule a new timer before this.
_backoffUntil: 0,
// Our current timer.
_timer: null,
// Our timer fires a promise - _timerRunning is true until it resolves or
// rejects.
_timerRunning: false,
// Our sync engine - XXX - maybe just a callback?
_engine: Sync,
// Our current "error backoff" timeout. zero if no error backoff is in
// progress and incremented after successive errors until a max is reached.
_currentErrorBackoff: 0,
// Our state variable and constants.
state: null,
STATE_OK: "ok",
STATE_ERROR_AUTHENTICATION: "authentication error",
STATE_ERROR_OTHER: "other error",
init() {
this.log.info("scheduler initialzing");
this._setupRLListener();
this._observe = this.observe.bind(this);
for (let notification of OBSERVERS) {
Services.obs.addObserver(this._observe, notification, false);
}
this._nextScheduledSync = Date.now() + intervals.initial;
this._setupTimer();
},
_setupRLListener() {
let maybeSync = () => {
if (this._timerRunning) {
// If a sync is currently running it is possible it will miss the change
// just made, so tell the timer the next sync should be 1 ms after
// it completes (we don't use zero as that has special meaning re backoffs)
this._maybeReschedule(1);
} else {
// Do the sync now.
this._syncNow();
}
};
let listener = {
onItemAdded: maybeSync,
onItemUpdated: maybeSync,
onItemDeleted: maybeSync,
}
this.readingList.addListener(listener);
},
// Note: only called by tests.
finalize() {
this.log.info("scheduler finalizing");
this._clearTimer();
for (let notification of OBSERVERS) {
Services.obs.removeObserver(this._observe, notification);
}
this._observe = null;
},
observe(subject, topic, data) {
this.log.debug("observed ${}", topic);
switch (topic) {
case "readinglist:backoff-requested": {
// The subject comes in as a string, a number of seconds.
let interval = parseInt(data, 10);
if (isNaN(interval)) {
this.log.warn("Backoff request had non-numeric value", data);
return;
}
this.log.info("Received a request to backoff for ${} seconds", interval);
this._backoffUntil = Date.now() + interval * 1000;
this._maybeReschedule(0);
break;
}
case "readinglist:user-sync":
this._syncNow();
break;
case "fxaccounts:onverified":
// If we were in an authentication error state, reset that now.
if (this.state == this.STATE_ERROR_AUTHENTICATION) {
this.state = this.STATE_OK;
}
// and sync now.
this._syncNow();
break;
// The rest just indicate that now is probably a good time to check if
// we can sync as normal using whatever schedule was previously set.
default:
break;
}
// When observers fire we ignore the current sync error state as the
// notification may indicate it's been resolved.
this._setupTimer(true);
},
// Is the current error state such that we shouldn't schedule a new sync.
_isBlockedOnError() {
// this needs more thought...
return this.state == this.STATE_ERROR_AUTHENTICATION;
},
// canSync indicates if we can currently sync.
_canSync(ignoreBlockingErrors = false) {
if (!prefs.get("enabled")) {
this.log.info("canSync=false - syncing is disabled");
return false;
}
if (Services.io.offline) {
this.log.info("canSync=false - we are offline");
return false;
}
if (!ignoreBlockingErrors && this._isBlockedOnError()) {
this.log.info("canSync=false - we are in a blocked error state", this.state);
return false;
}
this.log.info("canSync=true");
return true;
},
// _setupTimer checks the current state and the environment to see when
// we should next sync and creates the timer with the appropriate delay.
_setupTimer(ignoreBlockingErrors = false) {
if (!this._canSync(ignoreBlockingErrors)) {
this._clearTimer();
return;
}
if (this._timer) {
let when = new Date(this._nextScheduledSync);
let delay = this._nextScheduledSync - Date.now();
this.log.info("checkStatus - already have a timer - will fire in ${delay}ms at ${when}",
{delay, when});
return;
}
if (this._timerRunning) {
this.log.info("checkStatus - currently syncing");
return;
}
// no timer and we can sync, so start a new one.
let now = Date.now();
let delay = Math.max(0, this._nextScheduledSync - now);
let when = new Date(now + delay);
this.log.info("next scheduled sync is in ${delay}ms (at ${when})", {delay, when})
this._timer = this._setTimeout(delay);
},
// Something (possibly naively) thinks the next sync should happen in
// delay-ms. If there's a backoff in progress, ignore the requested delay
// and use the back-off. If there's already a timer scheduled for earlier
// than delay, let the earlier timer remain. Otherwise, use the requested
// delay.
_maybeReschedule(delay) {
// If there's no delay specified and there's nothing currently scheduled,
// it means a backoff request while the sync is actually running - there's
// no need to do anything here - the next reschedule after the sync
// completes will take the backoff into account.
if (!delay && !this._nextScheduledSync) {
this.log.debug("_maybeReschedule ignoring a backoff request while running");
return;
}
let now = Date.now();
if (!this._nextScheduledSync) {
this._nextScheduledSync = now + delay;
}
// If there is something currently scheduled before the requested delay,
// keep the existing value (eg, if we have a timer firing in 1 second, and
// get a notification that says we should sync in 2 seconds, we keep the 1
// second value)
this._nextScheduledSync = Math.min(this._nextScheduledSync, now + delay);
// But we still need to honor a backoff.
this._nextScheduledSync = Math.max(this._nextScheduledSync, this._backoffUntil);
// And always create a new timer next time _setupTimer is called.
this._clearTimer();
},
// callback for when the timer fires.
_doSync() {
this.log.debug("starting sync");
this._timer = null;
this._timerRunning = true;
// flag that there's no new schedule yet, so a request coming in while
// we are running does the right thing.
this._nextScheduledSync = 0;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
this._engine.start().then(() => {
this.log.info("Sync completed successfully");
// Write a pref in the same format used to services/sync to indicate
// the last success.
prefs.set("lastSync", new Date().toString());
this.state = this.STATE_OK;
this._logManager.resetFileLog().then(result => {
if (result == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
}
});
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
this._currentErrorBackoff = 0; // error retry interval is reset on success.
return intervals.schedule;
}).catch(err => {
// This isn't ideal - we really should have _canSync() check this - but
// that requires a refactor to turn _canSync() into a promise-based
// function.
if (err.message == fxAccountsCommon.ERROR_NO_ACCOUNT ||
err.message == fxAccountsCommon.ERROR_UNVERIFIED_ACCOUNT) {
// make everything look like success.
this._currentErrorBackoff = 0; // error retry interval is reset on success.
this.log.info("Can't sync due to FxA account state " + err.message);
this.state = this.STATE_OK;
this._logManager.resetFileLog().then(result => {
if (result == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
}
});
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
// it's unfortunate that we are probably going to hit this every
// 2 hours, but it should be invisible to the user.
return intervals.schedule;
}
this.state = err.message == fxAccountsCommon.ERROR_AUTH_ERROR ?
this.STATE_ERROR_AUTHENTICATION : this.STATE_ERROR_OTHER;
this.log.error("Sync failed, now in state '${state}': ${err}",
{state: this.state, err});
this._logManager.resetFileLog();
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
// We back-off on error retries until it hits our normally scheduled interval.
this._currentErrorBackoff = this._currentErrorBackoff == 0 ? intervals.retry :
Math.min(intervals.schedule, this._currentErrorBackoff * 2);
return this._currentErrorBackoff;
}).then(nextDelay => {
this._timerRunning = false;
// ensure a new timer is setup for the appropriate next time.
this._maybeReschedule(nextDelay);
this._setupTimer();
this._onAutoReschedule(); // just for tests...
}).catch(err => {
// We should never get here, but better safe than sorry...
this.log.error("Failed to reschedule after sync completed", err);
});
},
_clearTimer() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
},
// A function to "sync now", but not allowing it to start if one is
// already running, and rescheduling the timer.
// To call this, just send a "readinglist:user-sync" notification.
_syncNow() {
if (!prefs.get("enabled")) {
this.log.info("syncNow() but syncing is disabled - ignoring");
return;
}
if (this._timerRunning) {
this.log.info("syncNow() but a sync is already in progress - ignoring");
return;
}
this._clearTimer();
this._doSync();
},
// A couple of hook-points for testing.
// xpcshell tests hook this so (a) it can check the expected delay is set
// and (b) to ignore the delay and set a timeout of 0 so the test is fast.
_setTimeout(delay) {
return setTimeout(() => this._doSync(), delay);
},
// xpcshell tests hook this to make sure that the correct state etc exist
// after a sync has been completed and a new timer created (or not).
_onAutoReschedule() {},
};
let internalScheduler = new InternalScheduler();
internalScheduler.init();
// The public interface into this module is tiny, so a simple object that
// delegates to the implementation.
let ReadingListScheduler = {
get STATE_OK() internalScheduler.STATE_OK,
get STATE_ERROR_AUTHENTICATION() internalScheduler.STATE_ERROR_AUTHENTICATION,
get STATE_ERROR_OTHER() internalScheduler.STATE_ERROR_OTHER,
get state() internalScheduler.state,
};
// These functions are exposed purely for tests, which manage to grab them
// via a BackstagePass.
function createTestableScheduler(readingList) {
// kill the "real" scheduler as we don't want it listening to notifications etc.
if (internalScheduler) {
internalScheduler.finalize();
internalScheduler = null;
}
// No .init() call - that's up to the tests after hooking.
return new InternalScheduler(readingList);
}
// mochitests want the internal state of the real scheduler for various things.
function getInternalScheduler() {
return internalScheduler;
}

Просмотреть файл

@ -1,178 +0,0 @@
/* 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 client used to access the ReadingList server.
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
let log = Log.repository.getLogger("readinglist.serverclient");
const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
this.EXPORTED_SYMBOLS = [
"ServerClient",
];
// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
// encode the request as utf-8 even though it wants to know the encoding.
// It does, however, explicitly decode the response. This seems insane, but is
// what it is.
// The end result being we need to utf-8 the request and let the response take
// care of itself.
function objectToUTF8Json(obj) {
// FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
return CommonUtils.encodeUTF8(JSON.stringify(obj));
}
function ServerClient(fxa = fxAccounts) {
this.fxa = fxa;
}
ServerClient.prototype = {
request(options) {
return this._request(options.path, options.method, options.body, options.headers);
},
get serverURL() {
return Services.prefs.getCharPref("readinglist.server");
},
_getURL(path) {
let result = this.serverURL;
// we expect the path to have a leading slash, so remove any trailing
// slashes on the pref.
if (result.endsWith("/")) {
result = result.slice(0, -1);
}
return result + path;
},
// Hook points for testing.
_getToken() {
// Assume token-caching is in place - if it's not we should avoid doing
// this each request.
return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
},
_removeToken(token) {
return this.fxa.removeCachedOAuthToken({token});
},
// Converts an error from the RESTRequest object to an error we export.
_convertRestError(error) {
return error; // XXX - errors?
},
// Converts an error from a try/catch handler to an error we export.
_convertJSError(error) {
return error; // XXX - errors?
},
/*
* Perform a request - handles authentication
*/
_request: Task.async(function* (path, method, body, headers) {
let token = yield this._getToken();
let response = yield this._rawRequest(path, method, body, headers, token);
log.debug("initial request got status ${status}", response);
if (response.status == 401) {
// an auth error - assume our token has expired or similar.
this._removeToken(token);
token = yield this._getToken();
response = yield this._rawRequest(path, method, body, headers, token);
log.debug("retry of request got status ${status}", response);
}
return response;
}),
/*
* Perform a request *without* abstractions such as auth etc
*
* On success (which *includes* non-200 responses) returns an object like:
* {
* status: 200, # http status code
* headers: {}, # header values keyed by header name.
* body: {}, # parsed json
}
*/
_rawRequest(path, method, body, headers, oauthToken) {
return new Promise((resolve, reject) => {
let url = this._getURL(path);
log.debug("dispatching request to", url);
let request = new RESTRequest(url);
method = method.toUpperCase();
request.setHeader("Accept", "application/json");
request.setHeader("Content-Type", "application/json; charset=utf-8");
request.setHeader("Authorization", "Bearer " + oauthToken);
// and additional header specified for this request.
if (headers) {
for (let [headerName, headerValue] in Iterator(headers)) {
log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
request.setHeader(headerName, headerValue);
}
}
request.onComplete = error => {
// Although the server API docs say the "Backoff" header is on
// successful responses while "Retry-After" is on error responses, we
// just look for them both in both cases (as the scheduler makes no
// distinction)
let response = request.response;
if (response && response.headers) {
let backoff = response.headers["backoff"] || response.headers["retry-after"];
if (backoff) {
let numeric = backoff.toLowerCase() == "none" ? 0 :
parseInt(backoff, 10);
if (isNaN(numeric)) {
log.info("Server requested unrecognized backoff", backoff);
} else if (numeric > 0) {
log.info("Server requested backoff", numeric);
Services.obs.notifyObservers(null, "readinglist:backoff-requested", String(numeric));
}
}
}
if (error) {
return reject(this._convertRestError(error));
}
log.debug("received response status: ${status} ${statusText}", response);
// Handle response status codes we know about
let result = {
status: response.status,
headers: response.headers
};
try {
if (response.body) {
result.body = JSON.parse(response.body);
}
} catch (e) {
log.debug("Response is not JSON. First 1024 chars: |${body}|",
{ body: response.body.substr(0, 1024) });
// We don't reject due to this (and don't even make a huge amount of
// log noise - eg, a 50X error from a load balancer etc may not write
// JSON.
}
resolve(result);
}
// We are assuming the body has already been decoded and thus contains
// unicode, but the server expects utf-8. encodeURIComponent does that.
request.dispatch(method, objectToUTF8Json(body));
});
},
};

Просмотреть файл

@ -1,664 +0,0 @@
/* 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";
this.EXPORTED_SYMBOLS = [
"Sync",
];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ServerClient",
"resource:///modules/readinglist/ServerClient.jsm");
// The maximum number of sub-requests per POST /batch supported by the server.
// See http://readinglist.readthedocs.org/en/latest/api/batch.html.
const BATCH_REQUEST_LIMIT = 25;
// The Last-Modified header of server responses is stored here.
const SERVER_LAST_MODIFIED_HEADER_PREF = "readinglist.sync.serverLastModified";
// Maps local record properties to server record properties.
const SERVER_PROPERTIES_BY_LOCAL_PROPERTIES = {
guid: "id",
serverLastModified: "last_modified",
url: "url",
preview: "preview",
title: "title",
resolvedURL: "resolved_url",
resolvedTitle: "resolved_title",
excerpt: "excerpt",
archived: "archived",
deleted: "deleted",
favorite: "favorite",
isArticle: "is_article",
wordCount: "word_count",
unread: "unread",
addedBy: "added_by",
addedOn: "added_on",
storedOn: "stored_on",
markedReadBy: "marked_read_by",
markedReadOn: "marked_read_on",
readPosition: "read_position",
};
// Local record properties that can be uploaded in new items.
const NEW_RECORD_PROPERTIES = `
url
title
resolvedURL
resolvedTitle
excerpt
favorite
isArticle
wordCount
unread
addedBy
addedOn
markedReadBy
markedReadOn
readPosition
preview
`.trim().split(/\s+/);
// Local record properties that can be uploaded in changed items.
const MUTABLE_RECORD_PROPERTIES = `
title
resolvedURL
resolvedTitle
excerpt
favorite
isArticle
wordCount
unread
markedReadBy
markedReadOn
readPosition
preview
`.trim().split(/\s+/);
let log = Log.repository.getLogger("readinglist.sync");
/**
* An object that syncs reading list state with a server. To sync, make a new
* SyncImpl object and then call start() on it.
*
* @param readingList The ReadingList to sync.
*/
function SyncImpl(readingList) {
this.list = readingList;
this._client = new ServerClient();
}
/**
* This implementation uses the sync algorithm described here:
* https://github.com/mozilla-services/readinglist/wiki/Client-phases
* The "phases" mentioned in the methods below refer to the phases in that
* document.
*/
SyncImpl.prototype = {
/**
* Starts sync, if it's not already started.
*
* @return Promise<null> this.promise, i.e., a promise that will be resolved
* when sync completes, rejected on error.
*/
start() {
if (!this.promise) {
this.promise = Task.spawn(function* () {
try {
yield this._start();
} finally {
delete this.promise;
}
}.bind(this));
}
return this.promise;
},
/**
* A Promise<null> that will be non-null when sync is ongoing. Resolved when
* sync completes, rejected on error.
*/
promise: null,
/**
* See the document linked above that describes the sync algorithm.
*/
_start: Task.async(function* () {
log.info("Starting sync");
yield this._logDiagnostics();
yield this._uploadStatusChanges();
yield this._uploadNewItems();
yield this._uploadDeletedItems();
yield this._downloadModifiedItems();
// TODO: "Repeat [this phase] until no conflicts occur," says the doc.
yield this._uploadMaterialChanges();
log.info("Sync done");
}),
/**
* Phase 0 - for debugging we log some stuff about the local store before
* we start syncing.
* We only do this when the log level is "Trace" or lower as the info (a)
* may be expensive to generate, (b) generate alot of output and (c) may
* contain private information.
*/
_logDiagnostics: Task.async(function* () {
// Sadly our log is likely to have Log.Level.All, so loop over our
// appenders looking for the effective level.
let smallestLevel = log.appenders.reduce(
(prev, appender) => Math.min(prev, appender.level),
Log.Level.Error);
if (smallestLevel > Log.Level.Trace) {
return;
}
let localItems = [];
yield this.list.forEachItem(localItem => localItems.push(localItem));
log.trace("Have " + localItems.length + " local item(s)");
for (let localItem of localItems) {
// We need to use .record so we get access to a couple of the "internal" fields.
let record = localItem._record;
let redacted = {};
for (let attr of ["guid", "url", "resolvedURL", "serverLastModified", "syncStatus"]) {
redacted[attr] = record[attr];
}
log.trace(JSON.stringify(redacted));
}
// and the GUIDs of deleted items.
let deletedGuids = []
yield this.list.forEachSyncedDeletedGUID(guid => deletedGuids.push(guid));
// This might be a huge line, but that's OK.
log.trace("Have ${num} deleted item(s): ${deletedGuids}", {num: deletedGuids.length, deletedGuids});
}),
/**
* Phase 1 part 1
*
* Uploads not-new items with status-only changes. By design, status-only
* changes will never conflict with what's on the server.
*/
_uploadStatusChanges: Task.async(function* () {
log.debug("Phase 1 part 1: Uploading status changes");
yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_STATUS,
ReadingList.SyncStatusProperties.STATUS);
}),
/**
* There are two phases for uploading changed not-new items: one for items
* with status-only changes, one for items with material changes. The two
* work similarly mechanically, and this method is a helper for both.
*
* @param syncStatus Local items matching this sync status will be uploaded.
* @param localProperties An array of local record property names. The
* uploaded item records will include only these properties.
*/
_uploadChanges: Task.async(function* (syncStatus, localProperties) {
// Get local items that match the given syncStatus.
let requests = [];
yield this.list.forEachItem(localItem => {
requests.push({
path: "/articles/" + localItem.guid,
body: serverRecordFromLocalItem(localItem, localProperties),
});
}, { syncStatus: syncStatus });
if (!requests.length) {
log.debug("No local changes to upload");
return;
}
// Send the request.
let request = {
body: {
defaults: {
method: "PATCH",
},
requests: requests,
},
};
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse(true, "uploading changes", batchResponse);
return;
}
// Update local items based on the response.
for (let response of batchResponse.body.responses) {
if (response.status == 404) {
// item deleted
yield this._deleteItemForGUID(response.body.id);
continue;
}
if (response.status == 409) {
// "Conflict": A change violated a uniqueness constraint. Mark the item
// as having material changes, and reconcile and upload it in the
// material-changes phase.
// TODO
continue;
}
if (response.status != 200) {
this._handleUnexpectedResponse(false, "uploading a change", response);
continue;
}
// Don't assume the local record and the server record aren't materially
// different. Reconcile the differences.
// TODO
let item = yield this._itemForGUID(response.body.id);
yield this._updateItemWithServerRecord(item, response.body);
}
}),
/**
* Phase 1 part 2
*
* Uploads new items.
*/
_uploadNewItems: Task.async(function* () {
log.debug("Phase 1 part 2: Uploading new items");
// Get new local items.
let requests = [];
yield this.list.forEachItem(localItem => {
requests.push({
body: serverRecordFromLocalItem(localItem, NEW_RECORD_PROPERTIES),
});
}, { syncStatus: ReadingList.SyncStatus.NEW });
if (!requests.length) {
log.debug("No new local items to upload");
return;
}
// Send the request.
let request = {
body: {
defaults: {
method: "POST",
path: "/articles",
},
requests: requests,
},
};
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse(true, "uploading new items", batchResponse);
return;
}
// Update local items based on the response.
for (let response of batchResponse.body.responses) {
if (response.status == 303) {
// "See Other": An item with the URL already exists. Mark the item as
// having material changes, and reconcile and upload it in the
// material-changes phase.
// TODO
continue;
}
// Note that the server seems to return a 200 if an identical item already
// exists, but we shouldn't be uploading identical items in this phase in
// normal usage. But if something goes wrong locally (eg, we upload but
// get some error even though the upload worked) we will see this.
// So allow 200 but log a warning.
if (response.status == 200) {
log.debug("Attempting to upload a new item found the server already had it", response);
// but we still process it.
} else if (response.status != 201) {
this._handleUnexpectedResponse(false, "uploading a new item", response);
continue;
}
let item = yield this.list.itemForURL(response.body.url);
yield this._updateItemWithServerRecord(item, response.body);
}
}),
/**
* Phase 1 part 3
*
* Uploads deleted synced items.
*/
_uploadDeletedItems: Task.async(function* () {
log.debug("Phase 1 part 3: Uploading deleted items");
// Get deleted synced local items.
let requests = [];
yield this.list.forEachSyncedDeletedGUID(guid => {
requests.push({
path: "/articles/" + guid,
});
});
if (!requests.length) {
log.debug("No local deleted synced items to upload");
return;
}
// Send the request.
let request = {
body: {
defaults: {
method: "DELETE",
},
requests: requests,
},
};
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse(true, "uploading deleted items", batchResponse);
return;
}
// Delete local items based on the response.
for (let response of batchResponse.body.responses) {
// A 404 means the item was already deleted on the server, which is OK.
// We still need to make sure it's deleted locally, though.
if (response.status != 200 && response.status != 404) {
this._handleUnexpectedResponse(false, "uploading a deleted item", response);
continue;
}
yield this._deleteItemForGUID(response.body.id);
}
}),
/**
* Phase 2
*
* Downloads items that were modified since the last sync.
*/
_downloadModifiedItems: Task.async(function* () {
log.debug("Phase 2: Downloading modified items");
// Get modified items from the server.
let path = "/articles";
if (this._serverLastModifiedHeader) {
path += "?_since=" + this._serverLastModifiedHeader;
}
let request = {
method: "GET",
path: path,
};
let response = yield this._sendRequest(request);
if (response.status != 200) {
this._handleUnexpectedResponse(true, "downloading modified items", response);
return;
}
// Update local items based on the response.
for (let serverRecord of response.body.items) {
if (serverRecord.deleted) {
// _deleteItemForGUID is a no-op if no item exists with the GUID.
yield this._deleteItemForGUID(serverRecord.id);
continue;
}
let localItem = yield this._itemForGUID(serverRecord.id);
if (localItem) {
if (localItem.serverLastModified == serverRecord.last_modified) {
// We just uploaded this item in the new-items phase.
continue;
}
// The local item may have materially changed. In that case, don't
// overwrite the local changes with the server record. Instead, mark
// the item as having material changes and reconcile and upload it in
// the material-changes phase.
// TODO
yield this._updateItemWithServerRecord(localItem, serverRecord);
continue;
}
// A potentially new item. addItem() will fail here when an item was
// added to the local list between the time we uploaded new items and
// now.
let localRecord = localRecordFromServerRecord(serverRecord);
try {
yield this.list.addItem(localRecord);
} catch (ex) {
if (ex instanceof ReadingList.Error.Exists) {
log.debug("Tried to add an item that already exists.");
} else {
log.error("Error adding an item from server record ${serverRecord} ${ex}",
{ serverRecord, ex });
}
}
}
// Now that changes have been successfully applied, advance the server
// last-modified timestamp so that next time we fetch items starting from
// the current point. Response header names are lowercase.
if (response.headers && "last-modified" in response.headers) {
this._serverLastModifiedHeader = response.headers["last-modified"];
}
}),
/**
* Phase 3 (material changes)
*
* Uploads not-new items with material changes.
*/
_uploadMaterialChanges: Task.async(function* () {
log.debug("Phase 3: Uploading material changes");
yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_MATERIAL,
MUTABLE_RECORD_PROPERTIES);
}),
/**
* Gets the local ReadingListItem with the given GUID.
*
* @param guid The item's GUID.
* @return The matching ReadingListItem.
*/
_itemForGUID: Task.async(function* (guid) {
return (yield this.list.item({ guid: guid }));
}),
/**
* Updates the given local ReadingListItem with the given server record. The
* local item's sync status is updated to reflect the fact that the item has
* been synced and is up to date.
*
* @param item A local ReadingListItem.
* @param serverRecord A server record representing the item.
*/
_updateItemWithServerRecord: Task.async(function* (localItem, serverRecord) {
if (!localItem) {
// The item may have been deleted from the local list between the time we
// saw that it needed updating and now.
log.debug("Tried to update a null local item from server record",
serverRecord);
return;
}
localItem._record = localRecordFromServerRecord(serverRecord);
try {
yield this.list.updateItem(localItem);
} catch (ex) {
// The item may have been deleted from the local list after we fetched it.
if (ex instanceof ReadingList.Error.Deleted) {
log.debug("Tried to update an item that was deleted from server record",
serverRecord);
} else {
log.error("Error updating an item from server record ${serverRecord} ${ex}",
{ serverRecord, ex });
}
}
}),
/**
* Truly deletes the local ReadingListItem with the given GUID.
*
* @param guid The item's GUID.
*/
_deleteItemForGUID: Task.async(function* (guid) {
let item = yield this._itemForGUID(guid);
if (item) {
// If item is non-null, then it hasn't been deleted locally. Therefore
// it's important to delete it through its list so that the list and its
// consumers are notified properly. Set the syncStatus to NEW so that the
// list truly deletes the item.
item._record.syncStatus = ReadingList.SyncStatus.NEW;
try {
yield this.list.deleteItem(item);
} catch (ex) {
log.error("Failed delete local item with id ${guid} ${ex}",
{ guid, ex });
}
return;
}
// If item is null, then it may not actually exist locally, or it may have
// been synced and then deleted so that it's marked as being deleted. In
// that case, try to delete it directly from the store. As far as the list
// is concerned, the item has already been deleted.
log.debug("Item not present in list, deleting it by GUID instead");
try {
this.list._store.deleteItemByGUID(guid);
} catch (ex) {
log.error("Failed to delete local item with id ${guid} ${ex}",
{ guid, ex });
}
}),
/**
* Sends a request to the server.
*
* @param req The request object: { method, path, body, headers }.
* @return Promise<response> Resolved with the server's response object:
* { status, body, headers }.
*/
_sendRequest: Task.async(function* (req) {
log.debug("Sending request", req);
let response = yield this._client.request(req);
log.debug("Received response", response);
return response;
}),
/**
* The server limits the number of sub-requests in POST /batch'es to
* BATCH_REQUEST_LIMIT. This method takes an arbitrarily big batch request
* and breaks it apart into many individual batch requests in order to stay
* within the limit.
*
* @param bigRequest The same type of request object that _sendRequest takes.
* Since it's a POST /batch request, its `body` should have a
* `requests` property whose value is an array of sub-requests.
* `method` and `path` are automatically filled.
* @return Promise<response> Resolved when all requests complete with 200s, or
* when the first response that is not a 200 is received. In the
* first case, the resolved response is a combination of all the
* server responses, and response.body.responses contains the sub-
* responses for all the sub-requests in bigRequest. In the second
* case, the resolved response is the non-200 response straight from
* the server.
*/
_postBatch: Task.async(function* (bigRequest) {
log.debug("Sending batch requests");
let allSubResponses = [];
let remainingSubRequests = bigRequest.body.requests;
while (remainingSubRequests.length) {
let request = Object.assign({}, bigRequest);
request.method = "POST";
request.path = "/batch";
request.body.requests =
remainingSubRequests.splice(0, BATCH_REQUEST_LIMIT);
let response = yield this._sendRequest(request);
if (response.status != 200) {
return response;
}
allSubResponses = allSubResponses.concat(response.body.responses);
}
let bigResponse = {
status: 200,
body: {
responses: allSubResponses,
},
};
log.debug("All batch requests successfully sent");
return bigResponse;
}),
_handleUnexpectedResponse(isTopLevel, contextMsgFragment, response) {
log.error(`Unexpected response ${contextMsgFragment}`, response);
// We want to throw in some cases so the sync engine knows there was an
// error and retries using the error schedule. 401 implies an auth issue
// (possibly transient, possibly not) - but things like 404 might just
// relate to a single item and need not throw. Any 5XX implies a
// (hopefully transient) server error.
if (isTopLevel && (response.status == 401 || response.status >= 500)) {
throw new Error("Sync aborted due to " + response.status + " server response.");
}
},
// TODO: Wipe this pref when user logs out.
get _serverLastModifiedHeader() {
if (!("__serverLastModifiedHeader" in this)) {
this.__serverLastModifiedHeader =
Preferences.get(SERVER_LAST_MODIFIED_HEADER_PREF, undefined);
}
return this.__serverLastModifiedHeader;
},
set _serverLastModifiedHeader(val) {
this.__serverLastModifiedHeader = val;
Preferences.set(SERVER_LAST_MODIFIED_HEADER_PREF, val);
},
};
/**
* Translates a local ReadingListItem into a server record.
*
* @param localItem The local ReadingListItem.
* @param localProperties An array of local item property names. Only these
* properties will be included in the server record.
* @return The server record.
*/
function serverRecordFromLocalItem(localItem, localProperties) {
let serverRecord = {};
for (let localProp of localProperties) {
let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
if (localProp in localItem._record) {
serverRecord[serverProp] = localItem._record[localProp];
}
}
return serverRecord;
}
/**
* Translates a server record into a local record. The returned local record's
* syncStatus will reflect the fact that the local record is up-to-date synced.
*
* @param serverRecord The server record.
* @return The local record.
*/
function localRecordFromServerRecord(serverRecord) {
let localRecord = {
// Mark the record as being up-to-date synced.
syncStatus: ReadingList.SyncStatus.SYNCED,
};
for (let localProp in SERVER_PROPERTIES_BY_LOCAL_PROPERTIES) {
let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
if (serverProp in serverRecord) {
localRecord[localProp] = serverRecord[serverProp];
}
}
return localRecord;
}
Object.defineProperty(this, "Sync", {
get() {
if (!this._singleton) {
this._singleton = new SyncImpl(ReadingList);
}
return this._singleton;
},
});

Просмотреть файл

@ -1,24 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES.readinglist += [
'ReadingList.jsm',
'Scheduler.jsm',
'ServerClient.jsm',
'SQLiteStore.jsm',
'Sync.jsm',
]
TESTING_JS_MODULES += [
'test/ReadingListTestUtils.jsm',
]
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Reading List')

Просмотреть файл

@ -1,484 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("readinglist.sidebar");
let RLSidebar = {
/**
* Container element for all list item elements.
* @type {Element}
*/
list: null,
/**
* A promise that's resolved when building the initial list completes.
* @type {Promise}
*/
listPromise: null,
/**
* <template> element used for constructing list item elements.
* @type {Element}
*/
itemTemplate: null,
/**
* Map of ReadingList Item objects, keyed by their ID.
* @type {Map}
*/
itemsById: new Map(),
/**
* Map of list item elements, keyed by their corresponding Item's ID.
* @type {Map}
*/
itemNodesById: new Map(),
/**
* Initialize the sidebar UI.
*/
init() {
log.debug("Initializing");
addEventListener("unload", () => this.uninit());
this.list = document.getElementById("list");
this.emptyListInfo = document.getElementById("emptyListInfo");
this.itemTemplate = document.getElementById("item-template");
// click events for middle-clicks are not sent to DOM nodes, only to the document.
document.addEventListener("click", event => this.onClick(event));
this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
window.addEventListener("message", event => this.onMessage(event));
this.listPromise = this.ensureListItems();
ReadingList.addListener(this);
Services.prefs.setBoolPref("browser.readinglist.sidebarEverOpened", true);
let initEvent = new CustomEvent("Initialized", {bubbles: true});
document.documentElement.dispatchEvent(initEvent);
},
/**
* Un-initialize the sidebar UI.
*/
uninit() {
log.debug("Shutting down");
ReadingList.removeListener(this);
},
/**
* Handle an item being added to the ReadingList.
* TODO: We may not want to show this new item right now.
* TODO: We should guard against the list growing here.
*
* @param {ReadinglistItem} item - Item that was added.
*/
onItemAdded(item, append = false) {
log.trace(`onItemAdded: ${item}`);
let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
this.updateItem(item, itemNode);
// XXX Inserting at the top by default is a temp hack that will stop
// working once we start including items received from sync.
if (append)
this.list.appendChild(itemNode);
else
this.list.insertBefore(itemNode, this.list.firstChild);
this.itemNodesById.set(item.id, itemNode);
this.itemsById.set(item.id, item);
this.emptyListInfo.hidden = true;
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
itemNode.classList.add('visible');
});
});
},
/**
* Handle an item being deleted from the ReadingList.
* @param {ReadingListItem} item - Item that was deleted.
*/
onItemDeleted(item) {
log.trace(`onItemDeleted: ${item}`);
let itemNode = this.itemNodesById.get(item.id);
this.itemNodesById.delete(item.id);
this.itemsById.delete(item.id);
itemNode.addEventListener('transitionend', (event) => {
if (event.propertyName == "max-height") {
itemNode.remove();
// TODO: ensureListItems doesn't yet cope with needing to add one item.
//this.ensureListItems();
this.emptyListInfo.hidden = (this.numItems > 0);
}
}, false);
itemNode.classList.remove('visible');
},
/**
* Handle an item in the ReadingList having any of its properties changed.
* @param {ReadingListItem} item - Item that was updated.
*/
onItemUpdated(item) {
log.trace(`onItemUpdated: ${item}`);
let itemNode = this.itemNodesById.get(item.id);
if (!itemNode)
return;
this.updateItem(item, itemNode);
},
/**
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {ReadingListItem} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
updateItem(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
itemNode.setAttribute("title", `${item.title}\n${item.url}`);
itemNode.querySelector(".item-title").textContent = item.title;
let domain = item.uri.spec;
try {
domain = item.uri.host;
}
catch (err) {}
itemNode.querySelector(".item-domain").textContent = domain;
let thumb = itemNode.querySelector(".item-thumb-container");
if (item.preview) {
thumb.style.backgroundImage = "url(" + item.preview + ")";
} else {
thumb.style.removeProperty("background-image");
}
thumb.classList.toggle("preview-available", !!item.preview);
},
/**
* Ensure that the list is populated with the correct items.
*/
ensureListItems: Task.async(function* () {
yield ReadingList.forEachItem(item => {
// TODO: Should be batch inserting via DocumentFragment
try {
this.onItemAdded(item, true);
} catch (e) {
log.warn("Error adding item", e);
}
}, {sort: "addedOn", descending: true});
this.emptyListInfo.hidden = (this.numItems > 0);
}),
/**
* Get the number of items currently displayed in the list.
* @type {number}
*/
get numItems() {
return this.list.childElementCount;
},
/**
* The list item displayed in the current tab.
* @type {Element}
*/
get activeItem() {
return document.querySelector("#list > .item.active");
},
set activeItem(node) {
if (node && node.parentNode != this.list) {
log.error(`Unable to set activeItem to invalid node ${node}`);
return;
}
log.trace(`Setting activeItem: ${node ? node.id : null}`);
if (node && node.classList.contains("active")) {
return;
}
let prevItem = document.querySelector("#list > .item.active");
if (prevItem) {
prevItem.classList.remove("active");
}
if (node) {
node.classList.add("active");
}
let event = new CustomEvent("ActiveItemChanged", {bubbles: true});
this.list.dispatchEvent(event);
},
/**
* The list item selected with the keyboard.
* @type {Element}
*/
get selectedItem() {
return document.querySelector("#list > .item.selected");
},
set selectedItem(node) {
if (node && node.parentNode != this.list) {
log.error(`Unable to set selectedItem to invalid node ${node}`);
return;
}
log.trace(`Setting selectedItem: ${node ? node.id : null}`);
let prevItem = document.querySelector("#list > .item.selected");
if (prevItem) {
prevItem.classList.remove("selected");
}
if (node) {
node.classList.add("selected");
let itemId = this.getItemIdFromNode(node);
this.list.setAttribute("aria-activedescendant", "item-" + itemId);
} else {
this.list.removeAttribute("aria-activedescendant");
}
let event = new CustomEvent("SelectedItemChanged", {bubbles: true});
this.list.dispatchEvent(event);
},
/**
* The index of the currently selected item in the list.
* @type {number}
*/
get selectedIndex() {
for (let i = 0; i < this.numItems; i++) {
let item = this.list.children.item(i);
if (!item) {
break;
}
if (item.classList.contains("selected")) {
return i;
}
}
return -1;
},
set selectedIndex(index) {
log.trace(`Setting selectedIndex: ${index}`);
if (index == -1) {
this.selectedItem = null;
return;
}
let item = this.list.children.item(index);
if (!item) {
log.warn(`Unable to set selectedIndex to invalid index ${index}`);
return;
}
this.selectedItem = item;
},
/**
* Open a given URL. The event is used to determine where it should be opened
* (current tab, new tab, new window).
* @param {string} url - URL to open.
* @param {Event} event - KeyEvent or MouseEvent that triggered this action.
*/
openURL(url, event) {
log.debug(`Opening page ${url}`);
let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let currentUrl = mainWindow.gBrowser.currentURI.spec;
if (currentUrl.startsWith("about:reader"))
url = "about:reader?url=" + encodeURIComponent(url);
mainWindow.openUILink(url, event);
},
/**
* Get the ID of the Item associated with a given list item element.
* @param {element} node - List item element to get an ID for.
* @return {string} Assocated Item ID.
*/
getItemIdFromNode(node) {
let id = node.getAttribute("id");
if (id && id.startsWith("item-")) {
return id.slice(5);
}
return null;
},
/**
* Get the Item associated with a given list item element.
* @param {element} node - List item element to get an Item for.
* @return {string} Associated Item.
*/
getItemFromNode(node) {
let itemId = this.getItemIdFromNode(node);
if (!itemId) {
return null;
}
return this.itemsById.get(itemId);
},
/**
* Open the active item in the list.
* @param {Event} event - Event triggering this.
*/
openActiveItem(event) {
let itemNode = this.activeItem;
if (!itemNode) {
return;
}
let item = this.getItemFromNode(itemNode);
this.openURL(item.url, event);
},
/**
* Find the parent item element, from a given child element.
* @param {Element} node - Child element.
* @return {Element} Element for the item, or null if not found.
*/
findParentItemNode(node) {
while (node && node != this.list && node != document.documentElement &&
!node.classList.contains("item")) {
node = node.parentNode;
}
if (node != this.list && node != document.documentElement) {
return node;
}
return null;
},
/**
* Handle a click event on the sidebar.
* @param {Event} event - Triggering event.
*/
onClick(event) {
let itemNode = this.findParentItemNode(event.target);
if (!itemNode)
return;
if (event.target.classList.contains("remove-button")) {
ReadingList.deleteItem(this.getItemFromNode(itemNode));
return;
}
this.activeItem = itemNode;
this.openActiveItem(event);
},
/**
* Handle a mousemove event over the list box:
* If the hovered item isn't the selected one, clear the selection.
* @param {Event} event - Triggering event.
*/
onListMouseMove(event) {
let itemNode = this.findParentItemNode(event.target);
if (itemNode != this.selectedItem)
this.selectedItem = null;
},
/**
* Handle a keydown event on the list box.
* @param {Event} event - Triggering event.
*/
onListKeyDown(event) {
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
// TODO: Refactor this so we pass a direction to a generic method.
// See autocomplete.xml's getNextIndex
event.preventDefault();
if (!this.numItems) {
return;
}
let index = this.selectedIndex + 1;
if (index >= this.numItems) {
index = 0;
}
this.selectedIndex = index;
this.selectedItem.focus();
} else if (event.keyCode == KeyEvent.DOM_VK_UP) {
event.preventDefault();
if (!this.numItems) {
return;
}
let index = this.selectedIndex - 1;
if (index < 0) {
index = this.numItems - 1;
}
this.selectedIndex = index;
this.selectedItem.focus();
} else if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
let selectedItem = this.selectedItem;
if (selectedItem) {
this.activeItem = selectedItem;
this.openActiveItem(event);
}
}
},
/**
* Handle a message, typically sent from browser-readinglist.js
* @param {Event} event - Triggering event.
*/
onMessage(event) {
let msg = event.data;
if (msg.topic != "UpdateActiveItem") {
return;
}
if (!msg.url) {
this.activeItem = null;
} else {
ReadingList.itemForURL(msg.url).then(item => {
let node;
if (item && (node = this.itemNodesById.get(item.id))) {
this.activeItem = node;
}
});
}
}
};
addEventListener("DOMContentLoaded", () => RLSidebar.init());

Просмотреть файл

@ -1,34 +0,0 @@
<?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/. -->
<!DOCTYPE html [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="chrome://browser/content/readinglist/sidebar.js" type="application/javascript;version=1.8"></script>
<link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/readinglist/sidebar.css"/>
<title>&readingList.label;</title>
</head>
<body role="application">
<template id="item-template">
<div class="item" role="option" tabindex="-1">
<div class="item-thumb-container"></div>
<div class="item-summary-container">
<div class="item-title-lines">
<p class="item-title"/>
<button class="remove-button" title="&readingList.sidebar.delete.tooltip;"/>
</div>
<div class="item-domain"></div>
</div>
</div>
</template>
<div id="emptyListInfo" hidden="true">&readingList.sidebar.emptyText;</div>
<div id="list" role="listbox" tabindex="1"></div>
</body>
</html>

Просмотреть файл

@ -1,169 +0,0 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"ReadingListTestUtils",
];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
/** Preference name controlling whether the ReadingList feature is enabled/disabled. */
const PREF_RL_ENABLED = "browser.readinglist.enabled";
/**
* Utilities for testing the ReadingList sidebar.
*/
function SidebarUtils(window, assert) {
this.window = window;
this.Assert = assert;
}
SidebarUtils.prototype = {
/**
* Reference to the RLSidebar object controlling the ReadingList sidebar UI.
* @type {object}
*/
get RLSidebar() {
return this.window.SidebarUI.browser.contentWindow.RLSidebar;
},
/**
* Reference to the list container element in the sidebar.
* @type {Element}
*/
get list() {
return this.RLSidebar.list;
},
/**
* Opens the sidebar and waits until it finishes building its list.
* @return {Promise} Resolved when the sidebar's list is ready.
*/
showSidebar: Task.async(function* () {
yield this.window.ReadingListUI.showSidebar();
yield this.RLSidebar.listPromise;
}),
/**
* Check that the number of elements in the list matches the expected count.
* @param {number} count - Expected number of items.
*/
expectNumItems(count) {
this.Assert.equal(this.list.childElementCount, count,
"Should have expected number of items in the sidebar list");
},
/**
* Check all items in the sidebar list, ensuring the DOM matches the data.
*/
checkAllItems() {
for (let itemNode of this.list.children) {
this.checkSidebarItem(itemNode);
}
},
/**
* Run a series of sanity checks for an element in the list associated with
* an Item, ensuring the DOM matches the data.
*/
checkItem(node) {
let item = this.RLSidebar.getItemFromNode(node);
this.Assert.ok(node.classList.contains("item"),
"Node should have .item class");
this.Assert.equal(node.id, "item-" + item.id,
"Node should have correct ID");
this.Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url.spec,
"Node should have correct title attribute");
this.Assert.equal(node.querySelector(".item-title").textContent, item.title,
"Node's title element's text should match item title");
let domain = item.uri.spec;
try {
domain = item.uri.host;
}
catch (err) {}
this.Assert.equal(node.querySelector(".item-domain").textContent, domain,
"Node's domain element's text should match item title");
},
expectSelectedId(itemId) {
let selectedItem = this.RLSidebar.selectedItem;
if (itemId == null) {
this.Assert.equal(selectedItem, null, "Should have no selected item");
} else {
this.Assert.notEqual(selectedItem, null, "selectedItem should not be null");
let selectedId = this.RLSidebar.getItemIdFromNode(selectedItem);
this.Assert.equal(itemId, selectedId, "Should have currect item selected");
}
},
expectActiveId(itemId) {
let activeItem = this.RLSidebar.activeItem;
if (itemId == null) {
this.Assert.equal(activeItem, null, "Should have no active item");
} else {
this.Assert.notEqual(activeItem, null, "activeItem should not be null");
let activeId = this.RLSidebar.getItemIdFromNode(activeItem);
this.Assert.equal(itemId, activeId, "Should have correct item active");
}
},
};
/**
* Utilities for testing the ReadingList.
*/
this.ReadingListTestUtils = {
/**
* Whether the ReadingList feature is enabled or not.
* @type {boolean}
*/
get enabled() {
return Preferences.get(PREF_RL_ENABLED, false);
},
set enabled(value) {
Preferences.set(PREF_RL_ENABLED, !!value);
},
/**
* Utilities for testing the ReadingList sidebar.
*/
SidebarUtils: SidebarUtils,
/**
* Synthetically add an item to the ReadingList.
* @param {object|[object]} data - Object or array of objects to pass to the
* Item constructor.
* @return {Promise} Promise that gets fulfilled with the item or items added.
*/
addItem(data) {
if (Array.isArray(data)) {
let promises = [];
for (let itemData of data) {
promises.push(this.addItem(itemData));
}
return Promise.all(promises);
}
return ReadingList.addItem(data);
},
/**
* Cleanup all data, resetting to a blank state.
*/
cleanup: Task.async(function *() {
Preferences.reset(PREF_RL_ENABLED);
let items = [];
yield ReadingList.forEachItem(i => items.push(i));
for (let item of items) {
yield ReadingList.deleteItem(item);
}
}),
};

Просмотреть файл

@ -1,7 +0,0 @@
[DEFAULT]
support-files =
head.js
[browser_ui_enable_disable.js]
[browser_sidebar_list.js]
;[browser_sidebar_mouse_nav.js]

Просмотреть файл

@ -1,49 +0,0 @@
/**
* This tests the basic functionality of the sidebar to list items.
*/
add_task(function*() {
registerCleanupFunction(function*() {
ReadingListUI.hideSidebar();
yield RLUtils.cleanup();
});
RLUtils.enabled = true;
yield RLSidebarUtils.showSidebar();
let RLSidebar = RLSidebarUtils.RLSidebar;
let sidebarDoc = SidebarUI.browser.contentDocument;
Assert.equal(RLSidebar.numItems, 0, "Should start with no items");
Assert.equal(RLSidebar.activeItem, null, "Should start with no active item");
Assert.equal(RLSidebar.activeItem, null, "Should start with no selected item");
info("Adding first item");
yield RLUtils.addItem({
url: "http://example.com/article1",
title: "Article 1",
});
RLSidebarUtils.expectNumItems(1);
info("Adding more items");
yield RLUtils.addItem([{
url: "http://example.com/article2",
title: "Article 2",
}, {
url: "http://example.com/article3",
title: "Article 3",
}]);
RLSidebarUtils.expectNumItems(3);
info("Closing sidebar");
ReadingListUI.hideSidebar();
info("Adding another item");
yield RLUtils.addItem({
url: "http://example.com/article4",
title: "Article 4",
});
info("Re-opening sidebar");
yield RLSidebarUtils.showSidebar();
RLSidebarUtils.expectNumItems(4);
});

Просмотреть файл

@ -1,82 +0,0 @@
/**
* Test mouse navigation for selecting items in the sidebar.
*/
function mouseInteraction(mouseEvent, responseEvent, itemNode) {
let eventPromise = BrowserTestUtils.waitForEvent(RLSidebarUtils.list, responseEvent);
let details = {};
if (mouseEvent != "click") {
details.type = mouseEvent;
}
EventUtils.synthesizeMouseAtCenter(itemNode, details, itemNode.ownerDocument.defaultView);
return eventPromise;
}
add_task(function*() {
registerCleanupFunction(function*() {
ReadingListUI.hideSidebar();
yield RLUtils.cleanup();
});
RLUtils.enabled = true;
let itemData = [{
url: "http://example.com/article1",
title: "Article 1",
}, {
url: "http://example.com/article2",
title: "Article 2",
}, {
url: "http://example.com/article3",
title: "Article 3",
}, {
url: "http://example.com/article4",
title: "Article 4",
}, {
url: "http://example.com/article5",
title: "Article 5",
}];
info("Adding initial mock data");
yield RLUtils.addItem(itemData);
info("Fetching items");
let items = yield ReadingList.iterator({ sort: "url" }).items(itemData.length);
info("Opening sidebar");
yield RLSidebarUtils.showSidebar();
RLSidebarUtils.expectNumItems(5);
RLSidebarUtils.expectSelectedId(null);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 1");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(items[0].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 2");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[1]);
RLSidebarUtils.expectSelectedId(items[1].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 5");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[4]);
RLSidebarUtils.expectSelectedId(items[4].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 1 again");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(items[0].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse click on item 1");
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(items[0].id);
RLSidebarUtils.expectActiveId(items[0].id);
info("Mouse click on item 3");
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[2]);
RLSidebarUtils.expectSelectedId(items[2].id);
RLSidebarUtils.expectActiveId(items[2].id);
});

Просмотреть файл

@ -1,68 +0,0 @@
/**
* Test enabling/disabling the entire ReadingList feature via the
* browser.readinglist.enabled preference.
*/
function checkRLState() {
let enabled = RLUtils.enabled;
info("Checking ReadingList UI is " + (enabled ? "enabled" : "disabled"));
let sidebarBroadcaster = document.getElementById("readingListSidebar");
let sidebarMenuitem = document.getElementById("menu_readingListSidebar");
let bookmarksMenubarItem = document.getElementById("menu_readingList");
let bookmarksMenubarSeparator = document.getElementById("menu_readingListSeparator");
if (enabled) {
Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should not be hidden");
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
"Sidebar menuitem should be visible");
// Currently disabled on OSX.
if (bookmarksMenubarItem) {
Assert.notEqual(bookmarksMenubarItem.getAttribute("hidden"), "true",
"RL bookmarks submenu in menubar should not be hidden");
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
"RL bookmarks separator in menubar should be visible");
}
} else {
Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should be hidden");
Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
"Sidebar menuitem should be hidden");
Assert.equal(ReadingListUI.isSidebarOpen, false,
"ReadingListUI should not think sidebar is open");
// Currently disabled on OSX.
if (bookmarksMenubarItem) {
Assert.equal(bookmarksMenubarItem.getAttribute("hidden"), "true",
"RL bookmarks submenu in menubar should not be hidden");
Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
"RL bookmarks separator in menubar should be visible");
}
}
if (!enabled) {
Assert.equal(SidebarUI.isOpen, false, "Sidebar should not be open");
}
}
add_task(function*() {
info("Start with ReadingList disabled");
RLUtils.enabled = false;
checkRLState();
info("Enabling ReadingList");
RLUtils.enabled = true;
checkRLState();
info("Opening ReadingList sidebar");
yield ReadingListUI.showSidebar();
Assert.ok(SidebarUI.isOpen, "Sidebar should be open");
Assert.equal(SidebarUI.currentID, "readingListSidebar", "Sidebar should have ReadingList loaded");
info("Disabling ReadingList");
RLUtils.enabled = false;
Assert.ok(!SidebarUI.isOpen, "Sidebar should be closed");
checkRLState();
});

Просмотреть файл

@ -1,13 +0,0 @@
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingListTestUtils",
"resource://testing-common/ReadingListTestUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "RLUtils", () => {
return ReadingListTestUtils;
});
XPCOMUtils.defineLazyGetter(this, "RLSidebarUtils", () => {
return new RLUtils.SidebarUtils(window, Assert);
});

Просмотреть файл

@ -1,56 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
do_get_profile(); // fxa needs a profile directory for storage.
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
// Create a mocked FxAccounts object with a signed-in, verified user.
function* createMockFxA() {
function MockFxAccountsClient() {
this._email = "nobody@example.com";
this._verified = false;
this.accountStatus = function(uid) {
let deferred = Promise.defer();
deferred.resolve(!!uid && (!this._deletedOnServer));
return deferred.promise;
};
this.signOut = function() { return Promise.resolve(); };
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts() {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
getAssertion: () => Promise.resolve("assertion"),
});
}
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kA: "beef",
kB: "cafe",
verified: true
};
yield fxa.setSignedInUser(credentials);
return fxa;
}

Просмотреть файл

@ -1,782 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gDBFile = do_get_profile();
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
Cu.import("resource:///modules/readinglist/SQLiteStore.jsm");
Cu.import("resource://gre/modules/Sqlite.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Log.repository.getLogger("readinglist.api").level = Log.Level.All;
Log.repository.getLogger("readinglist.api").addAppender(new Log.DumpAppender());
var gList;
var gItems;
function run_test() {
run_next_test();
}
add_task(function* prepare() {
gList = ReadingList;
Assert.ok(gList);
gDBFile.append(gList._store.pathRelativeToProfileDir);
do_register_cleanup(function* () {
// Wait for the list's store to close its connection to the database.
yield gList.destroy();
if (gDBFile.exists()) {
gDBFile.remove(true);
}
});
gItems = [];
for (let i = 0; i < 3; i++) {
gItems.push({
guid: `guid${i}`,
url: `http://example.com/${i}`,
resolvedURL: `http://example.com/resolved/${i}`,
title: `title ${i}`,
excerpt: `excerpt ${i}`,
unread: 0,
favorite: 0,
isArticle: 1,
storedOn: Date.now(),
});
}
for (let item of gItems) {
let addedItem = yield gList.addItem(item);
checkItems(addedItem, item);
}
});
add_task(function* item_properties() {
// get an item
let iter = gList.iterator({
sort: "guid",
});
let item = (yield iter.items(1))[0];
Assert.ok(item);
Assert.ok(item.uri);
Assert.ok(item.uri instanceof Ci.nsIURI);
Assert.equal(item.uri.spec, item._record.url);
Assert.ok(item.resolvedURI);
Assert.ok(item.resolvedURI instanceof Ci.nsIURI);
Assert.equal(item.resolvedURI.spec, item._record.resolvedURL);
Assert.ok(item.addedOn);
Assert.ok(item.addedOn instanceof Cu.getGlobalForObject(ReadingList).Date);
Assert.ok(item.storedOn);
Assert.ok(item.storedOn instanceof Cu.getGlobalForObject(ReadingList).Date);
Assert.ok(typeof(item.favorite) == "boolean");
Assert.ok(typeof(item.isArticle) == "boolean");
Assert.ok(typeof(item.unread) == "boolean");
Assert.equal(item.id, hash(item._record.url));
});
add_task(function* constraints() {
// add an item again
let err = null;
try {
yield gList.addItem(gItems[0]);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
// add a new item with an existing guid
let item = kindOfClone(gItems[0]);
item.guid = gItems[0].guid;
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
// add a new item with an existing url
item = kindOfClone(gItems[0]);
item.url = gItems[0].url;
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
// add a new item with an existing resolvedURL
item = kindOfClone(gItems[0]);
item.resolvedURL = gItems[0].resolvedURL;
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
// add a new item with no url
item = kindOfClone(gItems[0]);
delete item.url;
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Error);
Assert.ok(!(err instanceof ReadingList.Error.Exists));
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
// update an item with no url
item = (yield gList.item({ guid: gItems[0].guid }));
Assert.ok(item);
let oldURL = item._record.url;
item._record.url = null;
err = null;
try {
yield gList.updateItem(item);
}
catch (e) {
err = e;
}
item._record.url = oldURL;
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Error);
Assert.ok(!(err instanceof ReadingList.Error.Exists));
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
// add an item with a bogus property
item = kindOfClone(gItems[0]);
item.bogus = "gnarly";
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Error);
Assert.ok(!(err instanceof ReadingList.Error.Exists));
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
// add a new item with no guid, which is allowed
item = kindOfClone(gItems[0]);
delete item.guid;
err = null;
let rlitem1;
try {
rlitem1 = yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
// add a second item with no guid, which is allowed
item = kindOfClone(gItems[1]);
delete item.guid;
err = null;
let rlitem2;
try {
rlitem2 = yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
// Delete the two previous items since other tests assume the store contains
// only gItems.
yield gList.deleteItem(rlitem1);
yield gList.deleteItem(rlitem2);
let items = [];
yield gList.forEachItem(i => items.push(i), { url: [rlitem1.uri.spec, rlitem2.uri.spec] });
Assert.equal(items.length, 0);
});
add_task(function* count() {
let count = yield gList.count();
Assert.equal(count, gItems.length);
count = yield gList.count({
guid: gItems[0].guid,
});
Assert.equal(count, 1);
});
add_task(function* forEachItem() {
// all items
let items = [];
yield gList.forEachItem(item => items.push(item), {
sort: "guid",
});
checkItems(items, gItems);
// first item
items = [];
yield gList.forEachItem(item => items.push(item), {
limit: 1,
sort: "guid",
});
checkItems(items, gItems.slice(0, 1));
// last item
items = [];
yield gList.forEachItem(item => items.push(item), {
limit: 1,
sort: "guid",
descending: true,
});
checkItems(items, gItems.slice(gItems.length - 1, gItems.length));
// match on a scalar property
items = [];
yield gList.forEachItem(item => items.push(item), {
guid: gItems[0].guid,
});
checkItems(items, gItems.slice(0, 1));
// match on an array
items = [];
yield gList.forEachItem(item => items.push(item), {
guid: gItems.map(i => i.guid),
sort: "guid",
});
checkItems(items, gItems);
// match on AND'ed properties
items = [];
yield gList.forEachItem(item => items.push(item), {
guid: gItems.map(i => i.guid),
title: gItems[0].title,
sort: "guid",
});
checkItems(items, [gItems[0]]);
// match on OR'ed properties
items = [];
yield gList.forEachItem(item => items.push(item), {
guid: gItems[1].guid,
sort: "guid",
}, {
guid: gItems[0].guid,
});
checkItems(items, [gItems[0], gItems[1]]);
// match on AND'ed and OR'ed properties
items = [];
yield gList.forEachItem(item => items.push(item), {
guid: gItems.map(i => i.guid),
title: gItems[1].title,
sort: "guid",
}, {
guid: gItems[0].guid,
});
checkItems(items, [gItems[0], gItems[1]]);
});
add_task(function* forEachSyncedDeletedItem() {
let deletedItem = yield gList.addItem({
guid: "forEachSyncedDeletedItem",
url: "http://example.com/forEachSyncedDeletedItem",
});
deletedItem._record.syncStatus = gList.SyncStatus.SYNCED;
yield gList.deleteItem(deletedItem);
let guids = [];
yield gList.forEachSyncedDeletedGUID(guid => guids.push(guid));
Assert.equal(guids.length, 1);
Assert.equal(guids[0], deletedItem.guid);
});
add_task(function* forEachItem_promises() {
// promises resolved immediately
let items = [];
yield gList.forEachItem(item => {
items.push(item);
return Promise.resolve();
}, {
sort: "guid",
});
checkItems(items, gItems);
// promises resolved after a delay
items = [];
let i = 0;
let promises = [];
yield gList.forEachItem(item => {
items.push(item);
// The previous promise should have been resolved by now.
if (i > 0) {
Assert.equal(promises[i - 1], null);
}
// Make a new promise that should continue iteration when resolved.
let this_i = i++;
let promise = new Promise(resolve => {
// Resolve the promise one second from now. The idea is that if
// forEachItem works correctly, then the callback should not be called
// again before the promise resolves -- before one second elapases.
// Maybe there's a better way to do this that doesn't hinge on timeouts.
setTimeout(() => {
promises[this_i] = null;
resolve();
}, 0);
});
promises.push(promise);
return promise;
}, {
sort: "guid",
});
checkItems(items, gItems);
});
add_task(function* iterator_forEach() {
// no limit
let items = [];
let iter = gList.iterator({
sort: "guid",
});
yield iter.forEach(item => items.push(item));
checkItems(items, gItems);
// limit one each time
items = [];
iter = gList.iterator({
sort: "guid",
});
for (let i = 0; i < gItems.length; i++) {
yield iter.forEach(item => items.push(item), 1);
checkItems(items, gItems.slice(0, i + 1));
}
yield iter.forEach(item => items.push(item), 100);
checkItems(items, gItems);
yield iter.forEach(item => items.push(item));
checkItems(items, gItems);
// match on a scalar property
items = [];
iter = gList.iterator({
sort: "guid",
guid: gItems[0].guid,
});
yield iter.forEach(item => items.push(item));
checkItems(items, [gItems[0]]);
// match on an array
items = [];
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
});
yield iter.forEach(item => items.push(item));
checkItems(items, gItems);
// match on AND'ed properties
items = [];
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
title: gItems[0].title,
});
yield iter.forEach(item => items.push(item));
checkItems(items, [gItems[0]]);
// match on OR'ed properties
items = [];
iter = gList.iterator({
sort: "guid",
guid: gItems[1].guid,
}, {
guid: gItems[0].guid,
});
yield iter.forEach(item => items.push(item));
checkItems(items, [gItems[0], gItems[1]]);
// match on AND'ed and OR'ed properties
items = [];
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
title: gItems[1].title,
}, {
guid: gItems[0].guid,
});
yield iter.forEach(item => items.push(item));
checkItems(items, [gItems[0], gItems[1]]);
});
add_task(function* iterator_items() {
// no limit
let iter = gList.iterator({
sort: "guid",
});
let items = yield iter.items(gItems.length);
checkItems(items, gItems);
items = yield iter.items(100);
checkItems(items, []);
// limit one each time
iter = gList.iterator({
sort: "guid",
});
for (let i = 0; i < gItems.length; i++) {
items = yield iter.items(1);
checkItems(items, gItems.slice(i, i + 1));
}
items = yield iter.items(100);
checkItems(items, []);
// match on a scalar property
iter = gList.iterator({
sort: "guid",
guid: gItems[0].guid,
});
items = yield iter.items(gItems.length);
checkItems(items, [gItems[0]]);
// match on an array
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
});
items = yield iter.items(gItems.length);
checkItems(items, gItems);
// match on AND'ed properties
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
title: gItems[0].title,
});
items = yield iter.items(gItems.length);
checkItems(items, [gItems[0]]);
// match on OR'ed properties
iter = gList.iterator({
sort: "guid",
guid: gItems[1].guid,
}, {
guid: gItems[0].guid,
});
items = yield iter.items(gItems.length);
checkItems(items, [gItems[0], gItems[1]]);
// match on AND'ed and OR'ed properties
iter = gList.iterator({
sort: "guid",
guid: gItems.map(i => i.guid),
title: gItems[1].title,
}, {
guid: gItems[0].guid,
});
items = yield iter.items(gItems.length);
checkItems(items, [gItems[0], gItems[1]]);
});
add_task(function* iterator_forEach_promise() {
// promises resolved immediately
let items = [];
let iter = gList.iterator({
sort: "guid",
});
yield iter.forEach(item => {
items.push(item);
return Promise.resolve();
});
checkItems(items, gItems);
// promises resolved after a delay
// See forEachItem_promises above for comments on this part.
items = [];
let i = 0;
let promises = [];
iter = gList.iterator({
sort: "guid",
});
yield iter.forEach(item => {
items.push(item);
if (i > 0) {
Assert.equal(promises[i - 1], null);
}
let this_i = i++;
let promise = new Promise(resolve => {
setTimeout(() => {
promises[this_i] = null;
resolve();
}, 0);
});
promises.push(promise);
return promise;
});
checkItems(items, gItems);
});
add_task(function* item() {
let item = yield gList.item({ guid: gItems[0].guid });
checkItems([item], [gItems[0]]);
item = yield gList.item({ guid: gItems[1].guid });
checkItems([item], [gItems[1]]);
});
add_task(function* itemForURL() {
let item = yield gList.itemForURL(gItems[0].url);
checkItems([item], [gItems[0]]);
item = yield gList.itemForURL(gItems[1].url);
checkItems([item], [gItems[1]]);
});
add_task(function* updateItem() {
// get an item
let items = [];
yield gList.forEachItem(i => items.push(i), {
guid: gItems[0].guid,
});
Assert.equal(items.length, 1);
let item = items[0];
// update its title
let newTitle = "updateItem new title";
Assert.notEqual(item.title, newTitle);
item.title = newTitle;
yield gList.updateItem(item);
// get the item again
items = [];
yield gList.forEachItem(i => items.push(i), {
guid: gItems[0].guid,
});
Assert.equal(items.length, 1);
item = items[0];
Assert.equal(item.title, newTitle);
});
add_task(function* item_setRecord() {
// get an item
let iter = gList.iterator({
sort: "guid",
});
let item = (yield iter.items(1))[0];
Assert.ok(item);
// Set item._record followed by an updateItem. After fetching the item again,
// its title should be the new title.
let newTitle = "item_setRecord title 1";
item._record.title = newTitle;
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
});
let sameItem = (yield iter.items(1))[0];
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, newTitle);
// Set item.title directly and call updateItem. After fetching the item
// again, its title should be the new title.
newTitle = "item_setRecord title 2";
item.title = newTitle;
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
});
sameItem = (yield iter.items(1))[0];
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, newTitle);
// Setting _record to an object with a bogus property should throw.
let err = null;
try {
item._record = { bogus: "gnarly" };
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("Unrecognized item property:") >= 0);
});
add_task(function* listeners() {
Assert.equal((yield gList.count()), gItems.length);
// add an item
let resolve;
let listenerPromise = new Promise(r => resolve = r);
let listener = {
onItemAdded: resolve,
};
gList.addListener(listener);
let item = kindOfClone(gItems[0]);
let items = yield Promise.all([listenerPromise, gList.addItem(item)]);
Assert.ok(items[0]);
Assert.ok(items[0] === items[1]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length + 1);
// update an item
listenerPromise = new Promise(r => resolve = r);
listener = {
onItemUpdated: resolve,
};
gList.addListener(listener);
items[0].title = "listeners new title";
yield gList.updateItem(items[0]);
let listenerItem = yield listenerPromise;
Assert.ok(listenerItem);
Assert.ok(listenerItem === items[0]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length + 1);
// delete an item
listenerPromise = new Promise(r => resolve = r);
listener = {
onItemDeleted: resolve,
};
gList.addListener(listener);
items[0].delete();
listenerItem = yield listenerPromise;
Assert.ok(listenerItem);
Assert.ok(listenerItem === items[0]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length);
});
// This test deletes items so it should probably run last of the 'gItems' tests...
add_task(function* deleteItem() {
// delete first item with item.delete()
let iter = gList.iterator({
sort: "guid",
});
let item = (yield iter.items(1))[0];
Assert.ok(item);
let {url, guid} = item;
Assert.ok((yield gList.itemForURL(url)), "should be able to get the item by URL before deletion");
Assert.ok((yield gList.item({guid})), "should be able to get the item by GUID before deletion");
yield item.delete();
try {
yield item.delete();
Assert.ok(false, "should not successfully delete the item a second time")
} catch(ex) {
Assert.ok(ex instanceof ReadingList.Error.Deleted);
}
Assert.ok(!(yield gList.itemForURL(url)), "should fail to get a deleted item by URL");
Assert.ok(!(yield gList.item({guid})), "should fail to get a deleted item by GUID");
gItems[0].list = null;
Assert.equal((yield gList.count()), gItems.length - 1);
let items = [];
yield gList.forEachItem(i => items.push(i), {
sort: "guid",
});
checkItems(items, gItems.slice(1));
// delete second item with list.deleteItem()
yield gList.deleteItem(items[0]);
try {
yield gList.deleteItem(items[0]);
Assert.ok(false, "should not successfully delete the item a second time")
} catch(ex) {
Assert.ok(ex instanceof ReadingList.Error.Deleted);
}
gItems[1].list = null;
Assert.equal((yield gList.count()), gItems.length - 2);
items = [];
yield gList.forEachItem(i => items.push(i), {
sort: "guid",
});
checkItems(items, gItems.slice(2));
// delete third item with list.deleteItem()
yield gList.deleteItem(items[0]);
gItems[2].list = null;
Assert.equal((yield gList.count()), gItems.length - 3);
items = [];
yield gList.forEachItem(i => items.push(i), {
sort: "guid",
});
checkItems(items, gItems.slice(3));
});
// Check that when we delete an item with a GUID it's no longer available as
// an item
add_task(function* deletedItemRemovedFromMap() {
yield gList.forEachItem(item => item.delete());
Assert.equal((yield gList.count()), 0);
let map = gList._itemsByNormalizedURL;
Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
let record = {
guid: "test-item",
url: "http://localhost",
syncStatus: gList.SyncStatus.SYNCED,
}
let item = yield gList.addItem(record);
Assert.equal(map.size, 1);
yield item.delete();
Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
// Now enumerate deleted items - should not come back.
yield gList.forEachSyncedDeletedGUID(() => {});
Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
});
function checkItems(actualItems, expectedItems) {
Assert.equal(actualItems.length, expectedItems.length);
for (let i = 0; i < expectedItems.length; i++) {
for (let prop in expectedItems[i]._record) {
Assert.ok(prop in actualItems[i]._record, prop);
Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
}
}
}
function kindOfClone(item) {
let newItem = {};
for (let prop in item) {
newItem[prop] = item[prop];
if (typeof(newItem[prop]) == "string") {
newItem[prop] += " -- make this string different";
}
}
return newItem;
}
function hash(str) {
let hasher = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stream = Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(Ci.nsIStringInputStream);
stream.data = str;
hasher.updateFromStream(stream, -1);
let binaryStr = hasher.finish(false);
let hexStr =
[("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in binaryStr)].
join("");
return hexStr;
}

Просмотреть файл

@ -1,333 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
Cu.import("resource:///modules/readinglist/SQLiteStore.jsm");
Cu.import("resource://gre/modules/Sqlite.jsm");
var gStore;
var gItems;
function run_test() {
run_next_test();
}
add_task(function* prepare() {
let basename = "reading-list-test.sqlite";
let dbFile = do_get_profile();
dbFile.append(basename);
function removeDB() {
if (dbFile.exists()) {
dbFile.remove(true);
}
}
removeDB();
do_register_cleanup(function* () {
// Wait for the store to close its connection to the database.
yield gStore.destroy();
removeDB();
});
gStore = new SQLiteStore(dbFile.path);
gItems = [];
for (let i = 0; i < 3; i++) {
gItems.push({
guid: `guid${i}`,
url: `http://example.com/${i}`,
resolvedURL: `http://example.com/resolved/${i}`,
title: `title ${i}`,
excerpt: `excerpt ${i}`,
unread: true,
addedOn: i,
});
}
for (let item of gItems) {
yield gStore.addItem(item);
}
});
add_task(function* constraints() {
// add an item again
let err = null;
try {
yield gStore.addItem(gItems[0]);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists:") >= 0);
// add a new item with an existing guid
function kindOfClone(item) {
let newItem = {};
for (let prop in item) {
newItem[prop] = item[prop];
if (typeof(newItem[prop]) == "string") {
newItem[prop] += " -- make this string different";
}
}
return newItem;
}
let item = kindOfClone(gItems[0]);
item.guid = gItems[0].guid;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
// add a new item with an existing url
item = kindOfClone(gItems[0]);
item.url = gItems[0].url;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists: url") >= 0);
// update an item with an existing url
item.guid = gItems[1].guid;
err = null;
try {
yield gStore.updateItem(item);
}
catch (e) {
err = e;
}
// The failure actually happens on items.guid, not items.url, because the item
// is first looked up by url, and then its other properties are updated on the
// resulting row.
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
// add a new item with an existing resolvedURL
item = kindOfClone(gItems[0]);
item.resolvedURL = gItems[0].resolvedURL;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
// update an item with an existing resolvedURL
item.url = gItems[1].url;
err = null;
try {
yield gStore.updateItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err instanceof ReadingList.Error.Exists);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
// add a new item with no guid, which is allowed
item = kindOfClone(gItems[0]);
delete item.guid;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
let url1 = item.url;
// add a second new item with no guid, which is allowed
item = kindOfClone(gItems[1]);
delete item.guid;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
let url2 = item.url;
// Delete both items since other tests assume the store contains only gItems.
yield gStore.deleteItemByURL(url1);
yield gStore.deleteItemByURL(url2);
let items = [];
yield gStore.forEachItem(i => items.push(i), [{ url: [url1, url2] }]);
Assert.equal(items.length, 0);
});
add_task(function* count() {
let count = yield gStore.count();
Assert.equal(count, gItems.length);
count = yield gStore.count([{
guid: gItems[0].guid,
}]);
Assert.equal(count, 1);
});
add_task(function* forEachItem() {
// all items
let items = [];
yield gStore.forEachItem(item => items.push(item), [{
sort: "guid",
}]);
checkItems(items, gItems);
// first item
items = [];
yield gStore.forEachItem(item => items.push(item), [{
limit: 1,
sort: "guid",
}]);
checkItems(items, gItems.slice(0, 1));
// last item
items = [];
yield gStore.forEachItem(item => items.push(item), [{
limit: 1,
sort: "guid",
descending: true,
}]);
checkItems(items, gItems.slice(gItems.length - 1, gItems.length));
// match on a scalar property
items = [];
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems[0].guid,
}]);
checkItems(items, gItems.slice(0, 1));
// match on an array
items = [];
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
sort: "guid",
}]);
checkItems(items, gItems);
// match on AND'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
title: gItems[0].title,
sort: "guid",
}]);
checkItems(items, [gItems[0]]);
// match on OR'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems[1].guid,
sort: "guid",
}, {
guid: gItems[0].guid,
}]);
checkItems(items, [gItems[0], gItems[1]]);
// match on AND'ed and OR'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
title: gItems[1].title,
sort: "guid",
}, {
guid: gItems[0].guid,
}]);
checkItems(items, [gItems[0], gItems[1]]);
});
add_task(function* updateItem() {
let newTitle = "a new title";
gItems[0].title = newTitle;
yield gStore.updateItem(gItems[0]);
let item;
yield gStore.forEachItem(i => item = i, [{
guid: gItems[0].guid,
}]);
Assert.ok(item);
Assert.equal(item.title, gItems[0].title);
});
add_task(function* updateItemByGUID() {
let newTitle = "updateItemByGUID";
gItems[0].title = newTitle;
yield gStore.updateItemByGUID(gItems[0]);
let item;
yield gStore.forEachItem(i => item = i, [{
guid: gItems[0].guid,
}]);
Assert.ok(item);
Assert.equal(item.title, gItems[0].title);
});
// This test deletes items so it should probably run last.
add_task(function* deleteItemByURL() {
// delete first item
yield gStore.deleteItemByURL(gItems[0].url);
Assert.equal((yield gStore.count()), gItems.length - 1);
let items = [];
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
}]);
checkItems(items, gItems.slice(1));
// delete second item
yield gStore.deleteItemByURL(gItems[1].url);
Assert.equal((yield gStore.count()), gItems.length - 2);
items = [];
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
}]);
checkItems(items, gItems.slice(2));
});
// This test deletes items so it should probably run last.
add_task(function* deleteItemByGUID() {
// delete third item
yield gStore.deleteItemByGUID(gItems[2].guid);
Assert.equal((yield gStore.count()), gItems.length - 3);
let items = [];
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
}]);
checkItems(items, gItems.slice(3));
});
function checkItems(actualItems, expectedItems) {
Assert.equal(actualItems.length, expectedItems.length);
for (let i = 0; i < expectedItems.length; i++) {
for (let prop in expectedItems[i]) {
Assert.ok(prop in actualItems[i], prop);
Assert.equal(actualItems[i][prop], expectedItems[i][prop]);
}
}
}

Просмотреть файл

@ -1,285 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource:///modules/readinglist/ServerClient.jsm");
Cu.import("resource://gre/modules/Log.jsm");
let appender = new Log.DumpAppender();
for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
Log.repository.getLogger(logName).addAppender(appender);
}
// Some test servers we use.
let Server = function(handlers) {
this._server = null;
this._handlers = handlers;
}
Server.prototype = {
start() {
this._server = new HttpServer();
for (let [path, handler] in Iterator(this._handlers)) {
// httpd.js seems to swallow exceptions
let thisHandler = handler;
let wrapper = (request, response) => {
try {
thisHandler(request, response);
} catch (ex) {
print("**** Handler for", path, "failed:", ex, ex.stack);
throw ex;
}
}
this._server.registerPathHandler(path, wrapper);
}
this._server.start(-1);
},
stop() {
return new Promise(resolve => {
this._server.stop(resolve);
this._server = null;
});
},
get host() {
return "http://localhost:" + this._server.identity.primaryPort;
},
};
// An OAuth server that hands out tokens.
function OAuthTokenServer() {
let server;
let handlers = {
"/v1/authorization": (request, response) => {
response.setStatusLine("1.1", 200, "OK");
let token = "token" + server.numTokenFetches;
print("Test OAuth server handing out token", token);
server.numTokenFetches += 1;
server.activeTokens.add(token);
response.write(JSON.stringify({access_token: token}));
},
"/v1/destroy": (request, response) => {
// Getting the body seems harder than it should be!
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(request.bodyInputStream);
let body = JSON.parse(sis.read(sis.available()));
sis.close();
let token = body.token;
ok(server.activeTokens.delete(token));
print("after destroy have", server.activeTokens.size, "tokens left.")
response.setStatusLine("1.1", 200, "OK");
response.write('{}');
},
}
server = new Server(handlers);
server.numTokenFetches = 0;
server.activeTokens = new Set();
return server;
}
function promiseObserver(topic) {
return new Promise(resolve => {
function observe(subject, topic, data) {
Services.obs.removeObserver(observe, topic);
resolve(data);
}
Services.obs.addObserver(observe, topic, false);
});
}
// The tests.
function run_test() {
run_next_test();
}
// Arrange for the first token we hand out to be rejected - the client should
// notice the 401 and silently get a new token and retry the request.
add_task(function testAuthRetry() {
let handlers = {
"/v1/batch": (request, response) => {
// We know the first token we will get is "token0", so we simulate that
// "expiring" by only accepting "token1". Then we just echo the response
// back.
let authHeader;
try {
authHeader = request.getHeader("Authorization");
} catch (ex) {}
if (authHeader != "Bearer token1") {
response.setStatusLine("1.1", 401, "Unauthorized");
response.write("wrong token");
return;
}
response.setStatusLine("1.1", 200, "OK");
response.write(JSON.stringify({ok: true}));
}
};
let rlserver = new Server(handlers);
rlserver.start();
let authServer = OAuthTokenServer();
authServer.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
let response = yield sc.request({
path: "/batch",
method: "post",
body: {foo: "bar"},
});
equal(response.status, 200, "got the 200 we expected");
equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
deepEqual(response.body, {ok: true});
} finally {
yield authServer.stop();
yield rlserver.stop();
}
});
// Check that specified headers are seen by the server, and that server headers
// in the response are seen by the client.
add_task(function testHeaders() {
let handlers = {
"/v1/batch": (request, response) => {
ok(request.hasHeader("x-foo"), "got our foo header");
equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
response.setHeader("Server-Sent-Header", "hello");
response.setStatusLine("1.1", 200, "OK");
response.write("{}");
}
};
let rlserver = new Server(handlers);
rlserver.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let response = yield sc.request({
path: "/batch",
method: "post",
headers: {"X-Foo": "bar"},
body: {foo: "bar"}});
equal(response.status, 200, "got the 200 we expected");
equal(response.headers["server-sent-header"], "hello", "got the server header");
} finally {
yield rlserver.stop();
}
});
// Check that a "backoff" header causes the correct notification.
add_task(function testBackoffHeader() {
let handlers = {
"/v1/batch": (request, response) => {
response.setHeader("Backoff", "123");
response.setStatusLine("1.1", 200, "OK");
response.write("{}");
}
};
let rlserver = new Server(handlers);
rlserver.start();
let observerPromise = promiseObserver("readinglist:backoff-requested");
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let response = yield sc.request({
path: "/batch",
method: "post",
headers: {"X-Foo": "bar"},
body: {foo: "bar"}});
equal(response.status, 200, "got the 200 we expected");
let data = yield observerPromise;
equal(data, "123", "got the expected header value.")
} finally {
yield rlserver.stop();
}
});
// Check that a "backoff" header causes the correct notification.
add_task(function testRetryAfterHeader() {
let handlers = {
"/v1/batch": (request, response) => {
response.setHeader("Retry-After", "456");
response.setStatusLine("1.1", 500, "Not OK");
response.write("{}");
}
};
let rlserver = new Server(handlers);
rlserver.start();
let observerPromise = promiseObserver("readinglist:backoff-requested");
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let response = yield sc.request({
path: "/batch",
method: "post",
headers: {"X-Foo": "bar"},
body: {foo: "bar"}});
equal(response.status, 500, "got the 500 we expected");
let data = yield observerPromise;
equal(data, "456", "got the expected header value.")
} finally {
yield rlserver.stop();
}
});
// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
// (Note the ServerClient assumes all strings in and out are UCS, and thus have
// already been encoded/decoded (ie, it never expects to receive stuff already
// utf-8 encoded, and never returns utf-8 encoded responses.)
add_task(function testUTF8() {
let handlers = {
"/v1/hello": (request, response) => {
// Get the body as bytes.
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(request.bodyInputStream);
let body = sis.read(sis.available());
sis.close();
// The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
// It should have been encoded as utf-8 which is \xc2\xa9
equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
// and just write it back unchanged.
response.setStatusLine("1.1", 200, "OK");
response.write(body);
}
};
let rlserver = new Server(handlers);
rlserver.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
let response = yield sc.request({
path: "/hello",
method: "post",
body: body
});
equal(response.status, 200, "got the 200 we expected");
deepEqual(response.body, body);
} finally {
yield rlserver.stop();
}
});

Просмотреть файл

@ -1,333 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gProfildDirFile = do_get_profile();
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource:///modules/readinglist/Sync.jsm");
let { localRecordFromServerRecord } =
Cu.import("resource:///modules/readinglist/Sync.jsm", {});
let gList;
let gSync;
let gClient;
let gLocalItems = [];
function run_test() {
run_next_test();
}
add_task(function* prepare() {
gSync = Sync;
gList = Sync.list;
let dbFile = gProfildDirFile.clone();
dbFile.append(gSync.list._store.pathRelativeToProfileDir);
do_register_cleanup(function* () {
// Wait for the list's store to close its connection to the database.
yield gList.destroy();
if (dbFile.exists()) {
dbFile.remove(true);
}
});
gClient = new MockClient();
gSync._client = gClient;
let dumpAppender = new Log.DumpAppender();
dumpAppender.level = Log.Level.All;
let logNames = [
"readinglist.sync",
];
for (let name of logNames) {
let log = Log.repository.getLogger(name);
log.level = Log.Level.All;
log.addAppender(dumpAppender);
}
});
add_task(function* uploadNewItems() {
// Add some local items.
for (let i = 0; i < 3; i++) {
let record = {
url: `http://example.com/${i}`,
title: `title ${i}`,
addedBy: "device name",
};
gLocalItems.push(yield gList.addItem(record));
}
Assert.ok(!("resolvedURL" in gLocalItems[0]._record));
yield gSync.start();
// The syncer should update local items with the items in the server response.
// e.g., the item didn't have a resolvedURL before sync, but after sync it
// should.
Assert.ok("resolvedURL" in gLocalItems[0]._record);
checkItems(gClient.items, gLocalItems);
});
add_task(function* uploadStatusChanges() {
// Change an item's unread from true to false.
Assert.ok(gLocalItems[0].unread === true);
gLocalItems[0].unread = false;
yield gList.updateItem(gLocalItems[0]);
yield gSync.start();
Assert.ok(gLocalItems[0].unread === false);
checkItems(gClient.items, gLocalItems);
});
add_task(function* downloadChanges() {
// Change an item on the server.
let newTitle = "downloadChanges new title";
let response = yield gClient.request({
method: "PATCH",
path: "/articles/1",
body: {
title: newTitle,
},
});
Assert.equal(response.status, 200);
// Add a new item on the server.
let newRecord = {
url: "http://example.com/downloadChanges-new-item",
title: "downloadChanges 2",
added_by: "device name",
};
response = yield gClient.request({
method: "POST",
path: "/articles",
body: newRecord,
});
Assert.equal(response.status, 201);
// Delete an item on the server.
response = yield gClient.request({
method: "DELETE",
path: "/articles/2",
});
Assert.equal(response.status, 200);
yield gSync.start();
// Refresh the list of local items. The changed item should be changed
// locally, the deleted item should be deleted locally, and the new item
// should appear in the list.
gLocalItems = (yield gList.iterator({ sort: "guid" }).
items(gLocalItems.length));
Assert.equal(gLocalItems[1].title, newTitle);
Assert.equal(gLocalItems[2].url, newRecord.url);
checkItems(gClient.items, gLocalItems);
});
function MockClient() {
this._items = [];
this._nextItemID = 0;
this._nextLastModifiedToken = 0;
}
MockClient.prototype = {
request(req) {
let response = this._routeRequest(req);
return new Promise(resolve => {
// Resolve the promise asyncly, just as if this were a real server, so
// that we don't somehow end up depending on sync behavior.
setTimeout(() => {
resolve(response);
}, 0);
});
},
get items() {
return this._items.slice().sort((item1, item2) => {
return item2.id < item1.id;
});
},
itemByID(id) {
return this._items.find(item => item.id == id);
},
itemByURL(url) {
return this._items.find(item => item.url == url);
},
_items: null,
_nextItemID: null,
_nextLastModifiedToken: null,
_routeRequest(req) {
for (let prop in this) {
let match = (new RegExp("^" + prop + "$")).exec(req.path);
if (match) {
let handler = this[prop];
let method = req.method.toLowerCase();
if (!(method in handler)) {
throw new Error(`Handler ${prop} does not support method ${method}`);
}
let response = handler[method].call(this, req.body, match);
// Make sure the response really is JSON'able (1) as a kind of sanity
// check, (2) to convert any non-primitives (e.g., new String()) into
// primitives, and (3) because that's what the real server returns.
response = JSON.parse(JSON.stringify(response));
return response;
}
}
throw new Error(`Unrecognized path: ${req.path}`);
},
// route handlers
"/articles": {
get(body) {
return new MockResponse(200, {
// No URL params supported right now.
items: this.items,
});
},
post(body) {
let existingItem = this.itemByURL(body.url);
if (existingItem) {
// The real server seems to return a 200 if the items are identical.
if (areSameItems(existingItem, body)) {
return new MockResponse(200);
}
// 303 see other
return new MockResponse(303, {
id: existingItem.id,
});
}
body.id = new String(this._nextItemID++);
let defaultProps = {
last_modified: this._nextLastModifiedToken,
preview: "",
resolved_url: body.url,
resolved_title: body.title,
excerpt: "",
archived: 0,
deleted: 0,
favorite: false,
is_article: true,
word_count: null,
unread: true,
added_on: null,
stored_on: this._nextLastModifiedToken,
marked_read_by: null,
marked_read_on: null,
read_position: null,
};
for (let prop in defaultProps) {
if (!(prop in body) || body[prop] === null) {
body[prop] = defaultProps[prop];
}
}
this._nextLastModifiedToken++;
this._items.push(body);
// 201 created
return new MockResponse(201, body);
},
},
"/articles/([^/]+)": {
get(body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
return new MockResponse(200, item);
},
patch(body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
for (let prop in body) {
item[prop] = body[prop];
}
item.last_modified = this._nextLastModifiedToken++;
return new MockResponse(200, item);
},
// There's a bug in pre-39's ES strict mode around forbidding the
// redefinition of reserved keywords that flags defining `delete` on an
// object as a syntax error. This weird syntax works around that.
["delete"](body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
item.deleted = true;
return new MockResponse(200);
},
},
"/batch": {
post(body) {
let responses = [];
let defaults = body.defaults || {};
for (let request of body.requests) {
for (let prop in defaults) {
if (!(prop in request)) {
request[prop] = defaults[prop];
}
}
responses.push(this._routeRequest(request));
}
return new MockResponse(200, {
defaults: defaults,
responses: responses,
});
},
},
};
function MockResponse(status, body, headers={}) {
this.status = status;
this.body = body;
this.headers = headers;
}
function areSameItems(item1, item2) {
for (let prop in item1) {
if (!(prop in item2) || item1[prop] != item2[prop]) {
return false;
}
}
for (let prop in item2) {
if (!(prop in item1) || item1[prop] != item2[prop]) {
return false;
}
}
return true;
}
function checkItems(serverRecords, localItems) {
serverRecords = serverRecords.map(r => localRecordFromServerRecord(r));
serverRecords = serverRecords.filter(r => !r.deleted);
Assert.equal(serverRecords.length, localItems.length);
for (let i = 0; i < serverRecords.length; i++) {
for (let prop in localItems[i]._record) {
Assert.ok(prop in serverRecords[i], prop);
Assert.equal(serverRecords[i][prop], localItems[i]._record[prop]);
}
}
}

Просмотреть файл

@ -1,255 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout',
'resource://gre/modules/Timer.jsm');
// Setup logging prefs before importing the scheduler module.
Services.prefs.setCharPref("readinglist.log.appender.dump", "Trace");
let {createTestableScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {});
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
// Log rotation needs a profile dir.
do_get_profile();
let prefs = new Preferences("readinglist.scheduler.");
prefs.set("enabled", true);
function promiseObserver(topic) {
return new Promise(resolve => {
let obs = (subject, topic, data) => {
Services.obs.removeObserver(obs, topic);
resolve(data);
}
Services.obs.addObserver(obs, topic, false);
});
}
function ReadingListMock() {
this.listener = null;
}
ReadingListMock.prototype = {
addListener(listener) {
ok(!this.listener, "mock only expects 1 listener");
this.listener = listener;
},
}
function createScheduler(options) {
// avoid typos in the test and other footguns in the options.
let allowedOptions = ["expectedDelay", "expectNewTimer", "syncFunction"];
for (let key of Object.keys(options)) {
if (allowedOptions.indexOf(key) == -1) {
throw new Error("Invalid option " + key);
}
}
let rlMock = new ReadingListMock();
let scheduler = createTestableScheduler(rlMock);
// make our hooks
let syncFunction = options.syncFunction || Promise.resolve;
scheduler._engine.start = syncFunction;
// we expect _setTimeout to be called *twice* - first is the initial sync,
// and there's no need to test the delay used for that. options.expectedDelay
// is to check the *subsequent* timer.
let numCalls = 0;
scheduler._setTimeout = function(delay) {
++numCalls;
print("Test scheduler _setTimeout call number " + numCalls + " with delay=" + delay);
switch (numCalls) {
case 1:
// this is the first and boring schedule as it initializes - do nothing
// other than return a timer that fires immediately.
return setTimeout(() => scheduler._doSync(), 0);
break;
case 2:
// This is the one we are interested in, so check things.
if (options.expectedDelay) {
// a little slop is OK as it takes a few ms to actually set the timer
ok(Math.abs(options.expectedDelay * 1000 - delay) < 500, [options.expectedDelay * 1000, delay]);
}
// and return a timeout that "never" fires
return setTimeout(() => scheduler._doSync(), 10000000);
break;
default:
// This is unexpected!
ok(false, numCalls);
}
};
// And a callback made once we've determined the next delay. This is always
// called even if _setTimeout isn't (due to no timer being created)
scheduler._onAutoReschedule = () => {
// Most tests expect a new timer, so this is "opt out"
let expectNewTimer = options.expectNewTimer === undefined ? true : options.expectNewTimer;
ok(expectNewTimer ? scheduler._timer : !scheduler._timer);
}
// calling .init fires things off...
scheduler.init();
return scheduler;
}
add_task(function* testSuccess() {
// promises which resolve once we've got all the expected notifications.
let allNotifications = [
promiseObserver("readinglist:sync:start"),
promiseObserver("readinglist:sync:finish"),
];
// New delay should be "as regularly scheduled".
prefs.set("schedule", 100);
let scheduler = createScheduler({expectedDelay: 100});
yield Promise.all(allNotifications);
scheduler.finalize();
});
// Test that if we get a reading list notification while we are syncing we
// immediately start a new one when it complets.
add_task(function* testImmediateResyncWhenChangedDuringSync() {
// promises which resolve once we've got all the expected notifications.
let allNotifications = [
promiseObserver("readinglist:sync:start"),
promiseObserver("readinglist:sync:finish"),
];
prefs.set("schedule", 100);
// New delay should be "immediate".
let scheduler = createScheduler({
expectedDelay: 0,
syncFunction: () => {
// we are now syncing - pretend the readinglist has an item change
scheduler.readingList.listener.onItemAdded();
return Promise.resolve();
}});
yield Promise.all(allNotifications);
scheduler.finalize();
});
add_task(function* testOffline() {
let scheduler = createScheduler({expectNewTimer: false});
Services.io.offline = true;
ok(!scheduler._canSync(), "_canSync is false when offline.")
ok(!scheduler._timer, "there is no current timer while offline.")
Services.io.offline = false;
ok(scheduler._canSync(), "_canSync is true when online.")
ok(scheduler._timer, "there is a new timer when back online.")
scheduler.finalize();
});
add_task(function* testRetryableError() {
let allNotifications = [
promiseObserver("readinglist:sync:start"),
promiseObserver("readinglist:sync:error"),
];
prefs.set("retry", 10);
let scheduler = createScheduler({
expectedDelay: 10,
syncFunction: () => Promise.reject("transient"),
});
yield Promise.all(allNotifications);
scheduler.finalize();
});
add_task(function* testAuthError() {
prefs.set("retry", 10);
// We expect an auth error to result in no new timer (as it's waiting for
// some indication it can proceed), but with the next delay being a normal
// "retry" interval (so when we can proceed it is probably already stale, so
// is effectively "immediate")
let scheduler = createScheduler({
expectedDelay: 10,
syncFunction: () => {
return Promise.reject(ReadingListScheduler._engine.ERROR_AUTHENTICATION);
},
expectNewTimer: false
});
// XXX - TODO - send an observer that "unblocks" us and ensure we actually
// do unblock.
scheduler.finalize();
});
add_task(function* testBackoff() {
let scheduler = createScheduler({expectedDelay: 1000});
Services.obs.notifyObservers(null, "readinglist:backoff-requested", 1000);
// XXX - this needs a little love as nothing checks createScheduler actually
// made the checks we think it does.
scheduler.finalize();
});
add_task(function testErrorBackoff() {
// This test can't sanely use the "test scheduler" above, so make one more
// suited.
let rlMock = new ReadingListMock();
let scheduler = createTestableScheduler(rlMock);
scheduler._setTimeout = function(delay) {
// create a timer that fires immediately
return setTimeout(() => scheduler._doSync(), 0);
}
// This does all the work...
function checkBackoffs(expectedSequences) {
let orig_maybeReschedule = scheduler._maybeReschedule;
return new Promise(resolve => {
let isSuccess = true; // ie, first run will put us in "fail" mode.
let expected;
function nextSequence() {
if (expectedSequences.length == 0) {
resolve();
return true; // we are done.
}
// setup the current set of expected results.
expected = expectedSequences.shift()
// and toggle the success status of the engine.
isSuccess = !isSuccess;
if (isSuccess) {
scheduler._engine.start = Promise.resolve;
} else {
scheduler._engine.start = () => {
return Promise.reject(new Error("oh no"))
}
}
return false; // not done.
};
// get the first sequence;
nextSequence();
// and setup the scheduler to check the sequences.
scheduler._maybeReschedule = function(nextDelay) {
let thisExpected = expected.shift();
equal(thisExpected * 1000, nextDelay);
if (expected.length == 0) {
if (nextSequence()) {
// we are done, so do nothing.
return;
}
}
// call the original impl to get the next schedule.
return orig_maybeReschedule.call(scheduler, nextDelay);
}
});
}
prefs.set("schedule", 100);
prefs.set("retry", 5);
// The sequences of timeouts we expect as the Sync error state changes.
let backoffsChecked = checkBackoffs([
// first sequence is in failure mode - expect the timeout to double until 'schedule'
[5, 10, 20, 40, 80, 100, 100],
// Sync just started working - more 'schedule'
[100, 100],
// Just stopped again - error backoff process restarts.
[5, 10],
// Another success and we are back to 'schedule'
[100, 100],
]);
// fire things off.
scheduler.init();
// and wait for completion.
yield backoffsChecked;
scheduler.finalize();
});
function run_test() {
run_next_test();
}

Просмотреть файл

@ -1,9 +0,0 @@
[DEFAULT]
head = head.js
firefox-appdir = browser
[test_ReadingList.js]
[test_ServerClient.js]
[test_scheduler.js]
[test_SQLiteStore.js]
[test_Sync.js]

Просмотреть файл

@ -379,9 +379,10 @@ this.UITour = {
},
onLocationChange: function(aLocation) {
// The ReadingList/ReaderView tour page is expected to run in Reader View,
// The ReaderView tour page is expected to run in Reader View,
// which disables JavaScript on the page. To get around that, we
// automatically start a pre-defined tour on page load.
// automatically start a pre-defined tour on page load (for hysterical
// raisins the ReaderView tour is known as "readinglist")
let originalUrl = ReaderMode.getOriginalUrl(aLocation);
if (this._readerViewTriggerRegEx.test(originalUrl)) {
this.startSubTour("readinglist");

Просмотреть файл

@ -26,6 +26,7 @@ CFLAGS="$CFLAGS -Wno-attributes"
CPPFLAGS="$CPPFLAGS -Wno-attributes"
CXXFLAGS="$CXXFLAGS -Wno-attributes"
TOOLTOOL_DIR="$(dirname $topsrcdir)"
export PKG_CONFIG_LIBDIR=/usr/lib64/pkgconfig:/usr/share/pkgconfig
. $topsrcdir/build/unix/mozconfig.gtk

Просмотреть файл

@ -7,10 +7,11 @@
"unpack": true
},
{
"size": 4079256,
"digest": "bb5238558bcf6db2ca395513c8dccaa15dd61b3c375598eb6a685356b0c1a2d9840e3bf81bc00242b872fd798541f53d723777c754412abf0e772b7cc284937c",
"size": 11179576,
"digest": "91567ce8e2bb8ab0ebc60c31e90731d88a1ea889fb71bcf55c735746a60fa7610b7e040ea3d8f727b6f692ae3ee703d6f3b30cdbd76fdf5617f77d9c38aa20ed",
"algorithm": "sha512",
"filename": "gtk3.tar.xz",
"setup": "setup.sh",
"unpack": true
},
{

Просмотреть файл

@ -1,16 +0,0 @@
[
{
"size": 80458572,
"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
"algorithm": "sha512",
"filename": "gcc.tar.xz",
"unpack": true
},
{
"size": 167175,
"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
"algorithm": "sha512",
"filename": "sccache.tar.bz2",
"unpack": true
}
]

Просмотреть файл

@ -8,5 +8,13 @@
"algorithm": "sha512",
"filename": "clang.tar.bz2",
"unpack": true
},
{
"size": 12057960,
"digest": "6105d6432943141cffb40020dc5ba3a793650bdeb3af9bd5e56d3796c5f03df9962a73e521646cd71fbfb5e266c1e74716ad722fb6055589dfb7d35175bca89e",
"algorithm": "sha512",
"filename": "gtk3.tar.xz",
"setup": "setup.sh",
"unpack": true
}
]

Просмотреть файл

@ -7,10 +7,11 @@
"unpack": true
},
{
"size": 4431740,
"digest": "68fc56b0fb0cdba629b95683d6649ff76b00dccf97af90960c3d7716f6108b2162ffd5ffcd5c3a60a21b28674df688fe4dabc67345e2da35ec5abeae3d48c8e3",
"size": 12057960,
"digest": "6105d6432943141cffb40020dc5ba3a793650bdeb3af9bd5e56d3796c5f03df9962a73e521646cd71fbfb5e266c1e74716ad722fb6055589dfb7d35175bca89e",
"algorithm": "sha512",
"filename": "gtk3.tar.xz",
"setup": "setup.sh",
"unpack": true
},
{

Просмотреть файл

@ -10,10 +10,11 @@
"unpack": true
},
{
"size": 4431740,
"digest": "68fc56b0fb0cdba629b95683d6649ff76b00dccf97af90960c3d7716f6108b2162ffd5ffcd5c3a60a21b28674df688fe4dabc67345e2da35ec5abeae3d48c8e3",
"size": 12057960,
"digest": "6105d6432943141cffb40020dc5ba3a793650bdeb3af9bd5e56d3796c5f03df9962a73e521646cd71fbfb5e266c1e74716ad722fb6055589dfb7d35175bca89e",
"algorithm": "sha512",
"filename": "gtk3.tar.xz",
"setup": "setup.sh",
"unpack": true
}
]

Просмотреть файл

@ -1,16 +0,0 @@
[
{
"size": 80458572,
"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
"algorithm": "sha512",
"filename": "gcc.tar.xz",
"unpack": true
},
{
"size": 167175,
"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
"algorithm": "sha512",
"filename": "sccache.tar.bz2",
"unpack": true
}
]

Просмотреть файл

@ -1667,7 +1667,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
text = L10N.getStr("networkMenu.sizeUnavailable");
}
else if(aValue === "cached") {
text = aValue;
text = L10N.getStr("networkMenu.sizeCached");
node.classList.add('theme-comment');
}
else {

Просмотреть файл

@ -11,8 +11,6 @@
<!ENTITY abouthome.pageTitle "&brandFullName; Start Page">
<!ENTITY abouthome.search.placeholder "Search">
<!-- LOCALIZATION NOTE (abouthome.defaultSnippet1.v1):
text in <a/> will be linked to the Firefox features page on mozilla.com
-->

Просмотреть файл

@ -864,18 +864,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY emeNotificationsDontAskAgain.label "Don't ask me again">
<!ENTITY emeNotificationsDontAskAgain.accesskey "D">
<!ENTITY readingList.label "Reading List">
<!ENTITY readingList.sidebar.commandKey "R">
<!ENTITY readingList.showSidebar.label "Show Reading List Sidebar">
<!-- Pre-landed string for bug 1124153 -->
<!ENTITY readingList.sidebar.showMore.label "Show more…">
<!-- Pre-landed string for bug 1133662 -->
<!ENTITY readingList.sidebar.emptyText "Add articles to your Reading List to save them for later and find them easily when you need them.">
<!ENTITY readingList.sidebar.delete.tooltip "Remove this from your Reading List">
<!-- Pre-landed strings for bug 1123519 -->
<!ENTITY readingList.sidebar.add.label "Add to Reading List">
<!ENTITY readingList.sidebar.add.tooltip "Add this page to your Reading List">
<!-- LOCALIZATION NOTE (saveToPocketCmd.label, saveLinkToPocketCmd.label, pocketMenuitem.label): Pocket is a brand name -->
<!ENTITY saveToPocketCmd.label "Save Page to Pocket">
<!ENTITY saveToPocketCmd.accesskey "k">

Просмотреть файл

@ -736,82 +736,10 @@ appmenu.updateFailed.description = Background update failed, please download upd
appmenu.restartBrowserButton.label = Restart %S
appmenu.downloadUpdateButton.label = Download Update
# LOCALIZATION NOTE : FILE Reading List and Reader View are feature names and therefore typically used as proper nouns.
# LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
# Pre-landed string for bug 1124153
# LOCALIZATION NOTE(readingList.sidebar.showMore.tooltip): %S is the number of items that will be added by clicking this button
# Semicolon-separated list of plural forms. See:
# http://developer.mozilla.org/en/docs/Localization_and_Plurals
readingList.sidebar.showMore.tooltip = Show %S more item;Show %S more items
# Pre-landed strings for bug 1131457 / bug 1131461
readingList.urlbar.add = Add page to Reading List
readingList.urlbar.addDone = Page added to Reading List
readingList.urlbar.remove = Remove page from Reading List
readingList.urlbar.removeDone = Page removed from Reading List
# Pre-landed strings for bug 1133610 & bug 1133611
# LOCALIZATION NOTE(readingList.promo.noSync.label): %S a link, using the text from readingList.promo.noSync.link
readingList.promo.noSync.label = Access your Reading List on all your devices. %S
# LOCALIZATION NOTE(readingList.promo.noSync.link): %S is syncBrandShortName
readingList.promo.noSync.link = Get started with %S.
# LOCALIZATION NOTE(readingList.promo.hasSync.label): %S is syncBrandShortName
readingList.promo.hasSync.label = You can now access your Reading List on all your devices connected by %S.
# Pre-landed strings for bug 1136570
readerView.promo.firstDetectedArticle.title = Read and save articles easily
readerView.promo.firstDetectedArticle.body = Click the book to make articles easier to read and use the plus to save them for later.
readingList.promo.firstUse.exitTourButton = Close
# LOCALIZATION NOTE(readingList.promo.firstUse.tourDoneButton):
# » is used as an indication that pressing this button progresses through the tour.
readingList.promo.firstUse.tourDoneButton = Start Reading »
# LOCALIZATION NOTE(readingList.promo.firstUse.readingList.multipleStepsTitle):
# This is used when there are multiple steps in the tour.
# %1$S is the current step's title (readingList.promo.firstUse.*.title), %2$S is the current step number of the tour, %3$S is the total number of steps.
readingList.promo.firstUse.multipleStepsTitle = %1$S (%2$S/%3$S)
readingList.promo.firstUse.readingList.title = Reading List
readingList.promo.firstUse.readingList.body = Save articles for later and find them easily when you need them.
# LOCALIZATION NOTE(readingList.promo.firstUse.readingList.moveToButton):
# » is used as an indication that pressing this button progresses through the tour.
readingList.promo.firstUse.readingList.moveToButton = Next: Easy finding »
readingList.promo.firstUse.readerView.title = Reader View
readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
# LOCALIZATION NOTE(readingList.promo.firstUse.readerView.moveToButton):
# » is used as an indication that pressing this button progresses through the tour.
readingList.promo.firstUse.readerView.moveToButton = Next: Easy reading »
readingList.promo.firstUse.syncNotSignedIn.title = Sync
# LOCALIZATION NOTE(readingList.promo.firstUse.syncNotSignedIn.body): %S is brandShortName
readingList.promo.firstUse.syncNotSignedIn.body = Sign in to access your Reading List everywhere you use %S.
# LOCALIZATION NOTE(readingList.promo.firstUse.syncNotSignedIn.moveToButton):
# » is used as an indication that pressing this button progresses through the tour.
readingList.promo.firstUse.syncNotSignedIn.moveToButton = Next: Easy access »
readingList.promo.firstUse.syncSignedIn.title = Sync
# LOCALIZATION NOTE(readingList.promo.firstUse.syncSignedIn.body): %S is brandShortName
readingList.promo.firstUse.syncSignedIn.body = Open your Reading List articles everywhere you use %S.
# LOCALIZATION NOTE(readingList.promo.firstUse.syncSignedIn.moveToButton):
# » is used as an indication that pressing this button progresses through the tour.
readingList.promo.firstUse.syncSignedIn.moveToButton = Next: Easy access »
# Pre-landed strings for bug 1136570
# LOCALIZATION NOTE(readingList.prepopulatedArticles.learnMore):
# This will show as an item in the Reading List, and will link to a page that explains and shows how the Reading List and Reader View works.
# This will be staged at:
# https://www.allizom.org/firefox/reading/start/
# And eventually available at:
# https://www.mozilla.org/firefox/reading/start/
# %S is brandShortName
readingList.prepopulatedArticles.learnMore = Learn how %S makes reading more pleasant
# LOCALIZATION NOTE(readingList.prepopulatedArticles.supportReadingList):
# This will show as an item in the Reading List, and will link to a SUMO article describing the Reading List:
# https://support.mozilla.org/kb/save-sync-and-read-pages-anywhere-reading-list
readingList.prepopulatedArticles.supportReadingList = Save, sync and read pages anywhere with Reading List
# LOCALIZATION NOTE(readingList.prepopulatedArticles.supportReaderView):
# This will show as an item in the Reading List, and will link to a SUMO article describing the Reader View:
# https://support.mozilla.org/kb/enjoy-clutter-free-web-pages-reader-view
readingList.prepopulatedArticles.supportReaderView = Enjoy clutter-free Web pages with Reader View
# LOCALIZATION NOTE(readingList.prepopulatedArticles.learnMore):
# This will show as an item in the Reading List, and will link to a SUMO article describing Sync:
# https://support.mozilla.org/kb/how-do-i-set-up-firefox-sync
# %S is syncBrandShortName
readingList.prepopulatedArticles.supportSync = Access your Reading List anywhere with %S
# LOCALIZATION NOTE (e10s.offerPopup.mainMessage
# e10s.offerPopup.highlight1

Просмотреть файл

@ -179,6 +179,11 @@ networkMenu.sizeKB=%S KB
# unavailable.
networkMenu.sizeUnavailable=
# LOCALIZATION NOTE (networkMenu.sizeCached): This is the label displayed
# in the network menu specifying the transferred of a request is
# cached.
networkMenu.sizeCached=cached
# LOCALIZATION NOTE (networkMenu.totalMS): This is the label displayed
# in the network menu specifying the time for a request to finish (in milliseconds).
networkMenu.totalMS=→ %S ms

Просмотреть файл

@ -26,8 +26,6 @@
<!ENTITY syncMy.label "Sync My">
<!ENTITY engine.bookmarks.label "Bookmarks">
<!ENTITY engine.bookmarks.accesskey "m">
<!ENTITY engine.readinglist.label "Reading List">
<!ENTITY engine.readinglist.accesskey "L">
<!ENTITY engine.tabs.label "Tabs">
<!ENTITY engine.tabs.accesskey "T">
<!ENTITY engine.history.label "History">

Просмотреть файл

@ -15,8 +15,6 @@
-->
<!ENTITY engine.bookmarks.label "Bookmarks">
<!ENTITY engine.bookmarks.accesskey "m">
<!ENTITY engine.readinglist.label "Reading List">
<!ENTITY engine.readinglist.accesskey "L">
<!ENTITY engine.history.label "History">
<!ENTITY engine.history.accesskey "r">
<!ENTITY engine.tabs.label "Tabs">

Просмотреть файл

@ -59,6 +59,15 @@ this.E10SUtils = {
mustLoadRemote = chromeReg.mustLoadURLRemotely(url);
}
if (aURL.startsWith("moz-extension:")) {
canLoadRemote = false;
mustLoadRemote = false;
}
if (aURL.startsWith("view-source:")) {
return this.canLoadURIInProcess(aURL.substr("view-source:".length), aProcess);
}
if (mustLoadRemote)
return processIsRemote;

Просмотреть файл

@ -16,7 +16,6 @@ Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
@ -48,23 +47,6 @@ let ReaderParent = {
receiveMessage: function(message) {
switch (message.name) {
case "Reader:AddToList": {
let article = message.data.article;
ReadingList.getMetadataFromBrowser(message.target).then(function(metadata) {
if (metadata.previews.length > 0) {
article.preview = metadata.previews[0];
}
ReadingList.addItem({
url: article.url,
title: article.title,
excerpt: article.excerpt,
preview: article.preview
});
});
break;
}
case "Reader:AddToPocket": {
let doc = message.target.ownerDocument;
let pocketWidget = doc.getElementById("pocket-button");
@ -114,24 +96,6 @@ let ReaderParent = {
}
break;
}
case "Reader:ListStatusRequest":
ReadingList.hasItemForURL(message.data.url).then(inList => {
let mm = message.target.messageManager
// Make sure the target browser is still alive before trying to send data back.
if (mm) {
mm.sendAsyncMessage("Reader:ListStatusData",
{ inReadingList: inList, url: message.data.url });
}
});
break;
case "Reader:RemoveFromList":
// We need to get the "real" item to delete it.
ReadingList.itemForURL(message.data.url).then(item => {
ReadingList.deleteItem(item)
});
break;
case "Reader:Share":
// XXX: To implement.
break;

Просмотреть файл

@ -520,11 +520,6 @@ menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png");
}
#menu_readingList,
#BMB_readingList {
list-style-image: url("chrome://browser/skin/readinglist/readinglist-icon.svg");
}
#panelMenu_pocket,
#menu_pocket,
#BMB_pocket {
@ -1287,8 +1282,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
list-style-image: url("chrome://browser/skin/Info.png");
}
%include ../shared/readinglist/readinglist.inc.css
/* Reader mode button */
#reader-mode-button {
@ -1951,3 +1944,7 @@ chatbox {
-moz-padding-end: 0 !important;
-moz-margin-end: 0 !important;
}
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
}

Просмотреть файл

@ -113,9 +113,6 @@ browser.jar:
skin/classic/browser/reader-tour.png (../shared/reader/reader-tour.png)
skin/classic/browser/reader-tour@2x.png (../shared/reader/reader-tour@2x.png)
skin/classic/browser/readerMode.svg (../shared/reader/readerMode.svg)
skin/classic/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
* skin/classic/browser/readinglist/sidebar.css (readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png (../shared/webrtc/webRTC-shareDevice-16.png)
skin/classic/browser/webRTC-shareDevice-16@2x.png (../shared/webrtc/webRTC-shareDevice-16@2x.png)
skin/classic/browser/webRTC-shareDevice-64.png (../shared/webrtc/webRTC-shareDevice-64.png)

Просмотреть файл

@ -1,41 +0,0 @@
/* 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 ../../shared/readinglist/sidebar.inc.css
html {
border: 1px solid ThreeDShadow;
background-color: -moz-Field;
color: -moz-FieldText;
box-sizing: border-box;
}
.item {
-moz-padding-end: 0;
}
.item.active {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
}
.item-title {
margin: 1px 0 0;
}
.item-title, .item-domain {
-moz-margin-end: 6px;
}
.remove-button {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 16, 16, 0);
}
.remove-button:hover {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 32, 16, 16);
}
.remove-button:hover:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
}

Просмотреть файл

@ -567,11 +567,6 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
}
}
/* #menu_readingList, svg icons don't work in the mac native menubar */
#BMB_readingList {
list-style-image: url("chrome://browser/skin/readinglist/readinglist-icon.svg");
}
#panelMenu_pocket,
#menu_pocket,
#BMB_pocket {
@ -2022,8 +2017,6 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
}
}
%include ../shared/readinglist/readinglist.inc.css
/* Reader mode button */
#reader-mode-button {
@ -3692,3 +3685,7 @@ window > chatbox {
padding-left: 0;
padding-right: 0;
}
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
}

Просмотреть файл

@ -150,9 +150,6 @@ browser.jar:
skin/classic/browser/reader-tour.png (../shared/reader/reader-tour.png)
skin/classic/browser/reader-tour@2x.png (../shared/reader/reader-tour@2x.png)
skin/classic/browser/readerMode.svg (../shared/reader/readerMode.svg)
skin/classic/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
* skin/classic/browser/readinglist/sidebar.css (readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png (../shared/webrtc/webRTC-shareDevice-16.png)
skin/classic/browser/webRTC-shareDevice-16@2x.png (../shared/webrtc/webRTC-shareDevice-16@2x.png)
skin/classic/browser/webRTC-shareDevice-64.png (../shared/webrtc/webRTC-shareDevice-64.png)

Просмотреть файл

@ -1,39 +0,0 @@
/* 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 ../../shared/readinglist/sidebar.inc.css
html {
border-top: 1px solid #bdbdbd;
}
.item-title {
margin: 4px 0 0;
}
.remove-button {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.png"), 0, 16, 16, 0);
}
.remove-button:hover {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.png"), 0, 32, 16, 16);
}
.remove-button:hover:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.png"), 0, 48, 16, 32);
}
@media (min-resolution: 2dppx) {
.remove-button {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close@2x.png"), 0, 32, 32, 0);
}
.remove-button:hover {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close@2x.png"), 0, 64, 32, 32);
}
.remove-button:hover:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close@2x.png"), 0, 96, 32, 64);
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше