зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1610641 - Remove all remaining Fennec modules. r=esawin
Differential Revision: https://phabricator.services.mozilla.com/D60586 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
576e9c816c
Коммит
45c0f04775
|
@ -25,8 +25,6 @@ module.exports = {
|
|||
"chrome/**",
|
||||
// Bug 1425048.
|
||||
"components/extensions/**",
|
||||
// Bug 1425034.
|
||||
"modules/WebsiteMetadata.jsm",
|
||||
],
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
|
|
|
@ -1,545 +0,0 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Home"];
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { SharedPreferences } = ChromeUtils.import(
|
||||
"resource://gre/modules/SharedPreferences.jsm"
|
||||
);
|
||||
const { EventDispatcher } = ChromeUtils.import(
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
|
||||
// Keep this in sync with the constant defined in PanelAuthCache.java
|
||||
const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
|
||||
|
||||
// Default weight for a banner message.
|
||||
const DEFAULT_WEIGHT = 100;
|
||||
|
||||
// See bug 915424
|
||||
function resolveGeckoURI(aURI) {
|
||||
if (!aURI) {
|
||||
throw new Error("Can't resolve an empty uri");
|
||||
}
|
||||
|
||||
if (aURI.startsWith("chrome://")) {
|
||||
let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
|
||||
Ci.nsIChromeRegistry
|
||||
);
|
||||
return registry.convertChromeURL(Services.io.newURI(aURI)).spec;
|
||||
} else if (aURI.startsWith("resource://")) {
|
||||
let handler = Services.io
|
||||
.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
return handler.resolveURI(Services.io.newURI(aURI));
|
||||
}
|
||||
return aURI;
|
||||
}
|
||||
|
||||
function BannerMessage(options) {
|
||||
let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(
|
||||
Ci.nsIUUIDGenerator
|
||||
);
|
||||
this.id = uuidgen.generateUUID().toString();
|
||||
|
||||
if ("text" in options && options.text != null) {
|
||||
this.text = options.text;
|
||||
}
|
||||
|
||||
if ("icon" in options && options.icon != null) {
|
||||
this.iconURI = resolveGeckoURI(options.icon);
|
||||
}
|
||||
|
||||
if ("onshown" in options && typeof options.onshown === "function") {
|
||||
this.onshown = options.onshown;
|
||||
}
|
||||
|
||||
if ("onclick" in options && typeof options.onclick === "function") {
|
||||
this.onclick = options.onclick;
|
||||
}
|
||||
|
||||
if ("ondismiss" in options && typeof options.ondismiss === "function") {
|
||||
this.ondismiss = options.ondismiss;
|
||||
}
|
||||
|
||||
let weight = parseInt(options.weight, 10);
|
||||
this.weight = weight > 0 ? weight : DEFAULT_WEIGHT;
|
||||
}
|
||||
|
||||
// We need this object to have access to the HomeBanner
|
||||
// private members without leaking it outside Home.jsm.
|
||||
var HomeBannerMessageHandlers;
|
||||
|
||||
var HomeBanner = (function() {
|
||||
// Whether there is a "HomeBanner:Get" request we couldn't fulfill.
|
||||
let _pendingRequest = false;
|
||||
|
||||
// Functions used to handle messages sent from Java.
|
||||
HomeBannerMessageHandlers = {
|
||||
"HomeBanner:Get": function handleBannerGet(data) {
|
||||
if (Object.keys(_messages).length > 0) {
|
||||
_sendBannerData();
|
||||
} else {
|
||||
_pendingRequest = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Holds the messages that will rotate through the banner.
|
||||
let _messages = {};
|
||||
|
||||
// Choose a random message from the set of messages, biasing towards those with higher weight.
|
||||
// Weight logic copied from desktop snippets:
|
||||
// https://github.com/mozilla/snippets-service/blob/7d80edb8b1cddaed075275c2fc7cdf69a10f4003/snippets/base/templates/base/includes/snippet_js.html#L119
|
||||
let _sendBannerData = function() {
|
||||
let totalWeight = 0;
|
||||
for (let key in _messages) {
|
||||
let message = _messages[key];
|
||||
totalWeight += message.weight;
|
||||
message.totalWeight = totalWeight;
|
||||
}
|
||||
|
||||
let threshold = Math.random() * totalWeight;
|
||||
for (let key in _messages) {
|
||||
let message = _messages[key];
|
||||
if (threshold < message.totalWeight) {
|
||||
EventDispatcher.instance
|
||||
.sendRequestForResult({
|
||||
type: "HomeBanner:Data",
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
iconURI: message.iconURI,
|
||||
})
|
||||
.then(id => _handleShown(id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _handleShown = function(id) {
|
||||
let message = _messages[id];
|
||||
if (message.onshown) {
|
||||
message.onshown();
|
||||
}
|
||||
};
|
||||
|
||||
let _handleClick = function(id) {
|
||||
let message = _messages[id];
|
||||
if (message.onclick) {
|
||||
message.onclick();
|
||||
}
|
||||
};
|
||||
|
||||
let _handleDismiss = function(id) {
|
||||
let message = _messages[id];
|
||||
if (message.ondismiss) {
|
||||
message.ondismiss();
|
||||
}
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
onEvent: function(event, data, callback) {
|
||||
switch (event) {
|
||||
case "HomeBanner:Click":
|
||||
_handleClick(data.id);
|
||||
break;
|
||||
|
||||
case "HomeBanner:Dismiss":
|
||||
_handleDismiss(data.id);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new banner message to the rotation.
|
||||
*
|
||||
* @return id Unique identifer for the message.
|
||||
*/
|
||||
add: function(options) {
|
||||
let message = new BannerMessage(options);
|
||||
_messages[message.id] = message;
|
||||
|
||||
// If this is the first message we're adding, add
|
||||
// observers to listen for requests from the Java UI.
|
||||
if (Object.keys(_messages).length == 1) {
|
||||
EventDispatcher.instance.registerListener(this, [
|
||||
"HomeBanner:Click",
|
||||
"HomeBanner:Dismiss",
|
||||
]);
|
||||
|
||||
// Send a message to Java if there's a pending "HomeBanner:Get" request.
|
||||
if (_pendingRequest) {
|
||||
_pendingRequest = false;
|
||||
_sendBannerData();
|
||||
}
|
||||
}
|
||||
|
||||
return message.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a banner message from the rotation.
|
||||
*
|
||||
* @param id The id of the message to remove.
|
||||
*/
|
||||
remove: function(id) {
|
||||
if (!(id in _messages)) {
|
||||
throw new Error(
|
||||
"Home.banner: Can't remove message that doesn't exist: id = " + id
|
||||
);
|
||||
}
|
||||
|
||||
delete _messages[id];
|
||||
|
||||
// If there are no more messages, remove the observers.
|
||||
if (Object.keys(_messages).length == 0) {
|
||||
EventDispatcher.instance.unregisterListener(this, [
|
||||
"HomeBanner:Click",
|
||||
"HomeBanner:Dismiss",
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
// We need this object to have access to the HomePanels
|
||||
// private members without leaking it outside Home.jsm.
|
||||
var HomePanelsMessageHandlers;
|
||||
|
||||
var HomePanels = (function() {
|
||||
// Functions used to handle messages sent from Java.
|
||||
HomePanelsMessageHandlers = {
|
||||
"HomePanels:Get": function handlePanelsGet(data) {
|
||||
let requestId = data.requestId;
|
||||
let ids = data.ids || null;
|
||||
|
||||
let panels = [];
|
||||
for (let id in _registeredPanels) {
|
||||
// Null ids means we want to fetch all available panels
|
||||
if (ids == null || ids.includes(id)) {
|
||||
try {
|
||||
panels.push(_generatePanel(id));
|
||||
} catch (e) {
|
||||
Cu.reportError(
|
||||
"Home.panels: Invalid options, panel.id = " + id + ": " + e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "HomePanels:Data",
|
||||
panels: panels,
|
||||
requestId: requestId,
|
||||
});
|
||||
},
|
||||
|
||||
"HomePanels:Authenticate": function handlePanelsAuthenticate(data) {
|
||||
// Generate panel options to get auth handler.
|
||||
let id = data.id;
|
||||
let options = _registeredPanels[id]();
|
||||
if (!options.auth) {
|
||||
throw new Error("Home.panels: Invalid auth for panel.id = " + id);
|
||||
}
|
||||
if (
|
||||
!options.auth.authenticate ||
|
||||
typeof options.auth.authenticate !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid auth authenticate function: panel.id = " +
|
||||
this.id
|
||||
);
|
||||
}
|
||||
options.auth.authenticate();
|
||||
},
|
||||
|
||||
"HomePanels:RefreshView": function handlePanelsRefreshView(data) {
|
||||
let options = _registeredPanels[data.panelId]();
|
||||
let view = options.views[data.viewIndex];
|
||||
|
||||
if (!view) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid view for panel.id = " +
|
||||
`${data.panelId}, view.index = ${data.viewIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!view.onrefresh || typeof view.onrefresh !== "function") {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid onrefresh for panel.id = " +
|
||||
`${data.panelId}, view.index = ${data.viewIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
view.onrefresh();
|
||||
},
|
||||
|
||||
"HomePanels:Installed": function handlePanelsInstalled(data) {
|
||||
let id = data.id;
|
||||
_assertPanelExists(id);
|
||||
|
||||
let options = _registeredPanels[id]();
|
||||
if (!options.oninstall) {
|
||||
return;
|
||||
}
|
||||
if (typeof options.oninstall !== "function") {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid oninstall function: panel.id = " + this.id
|
||||
);
|
||||
}
|
||||
options.oninstall();
|
||||
},
|
||||
|
||||
"HomePanels:Uninstalled": function handlePanelsUninstalled(data) {
|
||||
let id = data.id;
|
||||
_assertPanelExists(id);
|
||||
|
||||
let options = _registeredPanels[id]();
|
||||
if (!options.onuninstall) {
|
||||
return;
|
||||
}
|
||||
if (typeof options.onuninstall !== "function") {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid onuninstall function: panel.id = " + this.id
|
||||
);
|
||||
}
|
||||
options.onuninstall();
|
||||
},
|
||||
};
|
||||
|
||||
// Holds the current set of registered panels that can be
|
||||
// installed, updated, uninstalled, or unregistered. It maps
|
||||
// panel ids with the functions that dynamically generate
|
||||
// their respective panel options. This is used to retrieve
|
||||
// the current list of available panels in the system.
|
||||
// See HomePanels:Get handler.
|
||||
let _registeredPanels = {};
|
||||
|
||||
// Valid layouts for a panel.
|
||||
let Layout = Object.freeze({
|
||||
FRAME: "frame",
|
||||
});
|
||||
|
||||
// Valid types of views for a dataset.
|
||||
let View = Object.freeze({
|
||||
LIST: "list",
|
||||
GRID: "grid",
|
||||
});
|
||||
|
||||
// Valid item types for a panel view.
|
||||
let Item = Object.freeze({
|
||||
ARTICLE: "article",
|
||||
IMAGE: "image",
|
||||
ICON: "icon",
|
||||
});
|
||||
|
||||
// Valid item handlers for a panel view.
|
||||
let ItemHandler = Object.freeze({
|
||||
BROWSER: "browser",
|
||||
INTENT: "intent",
|
||||
});
|
||||
|
||||
function Panel(id, options) {
|
||||
this.id = id;
|
||||
this.title = options.title;
|
||||
this.layout = options.layout;
|
||||
this.views = options.views;
|
||||
this.default = !!options.default;
|
||||
|
||||
if (!this.id || !this.title) {
|
||||
throw new Error(
|
||||
"Home.panels: Can't create a home panel without an id and title!"
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.layout) {
|
||||
// Use FRAME layout by default
|
||||
this.layout = Layout.FRAME;
|
||||
} else if (!_valueExists(Layout, this.layout)) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid layout for panel: panel.id = " +
|
||||
`${this.id}, panel.layout =${this.layout}`
|
||||
);
|
||||
}
|
||||
|
||||
for (let view of this.views) {
|
||||
if (!_valueExists(View, view.type)) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid view type: panel.id = " +
|
||||
`${this.id}, view.type = ${view.type}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!view.itemType) {
|
||||
if (view.type == View.LIST) {
|
||||
// Use ARTICLE item type by default in LIST views
|
||||
view.itemType = Item.ARTICLE;
|
||||
} else if (view.type == View.GRID) {
|
||||
// Use IMAGE item type by default in GRID views
|
||||
view.itemType = Item.IMAGE;
|
||||
}
|
||||
} else if (!_valueExists(Item, view.itemType)) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid item type: panel.id = " +
|
||||
`${this.id}, view.itemType = ${view.itemType}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!view.itemHandler) {
|
||||
// Use BROWSER item handler by default
|
||||
view.itemHandler = ItemHandler.BROWSER;
|
||||
} else if (!_valueExists(ItemHandler, view.itemHandler)) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid item handler: panel.id = " +
|
||||
`${this.id}, view.itemHandler = ${view.itemHandler}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!view.dataset) {
|
||||
throw new Error(
|
||||
"Home.panels: No dataset provided for view: panel.id = " +
|
||||
`${this.id}, view.type = ${view.type}`
|
||||
);
|
||||
}
|
||||
|
||||
if (view.onrefresh) {
|
||||
view.refreshEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.auth) {
|
||||
if (!options.auth.messageText) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid auth messageText: panel.id = " + this.id
|
||||
);
|
||||
}
|
||||
if (!options.auth.buttonText) {
|
||||
throw new Error(
|
||||
"Home.panels: Invalid auth buttonText: panel.id = " + this.id
|
||||
);
|
||||
}
|
||||
|
||||
this.authConfig = {
|
||||
messageText: options.auth.messageText,
|
||||
buttonText: options.auth.buttonText,
|
||||
};
|
||||
|
||||
// Include optional image URL if it is specified.
|
||||
if (options.auth.imageUrl) {
|
||||
this.authConfig.imageUrl = options.auth.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.position >= 0) {
|
||||
this.position = options.position;
|
||||
}
|
||||
}
|
||||
|
||||
let _generatePanel = function(id) {
|
||||
let options = _registeredPanels[id]();
|
||||
return new Panel(id, options);
|
||||
};
|
||||
|
||||
// Helper function used to see if a value is in an object.
|
||||
let _valueExists = function(obj, value) {
|
||||
for (let key in obj) {
|
||||
if (obj[key] == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let _assertPanelExists = function(id) {
|
||||
if (!(id in _registeredPanels)) {
|
||||
throw new Error("Home.panels: Panel doesn't exist: id = " + id);
|
||||
}
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
Layout: Layout,
|
||||
View: View,
|
||||
Item: Item,
|
||||
ItemHandler: ItemHandler,
|
||||
|
||||
register: function(id, optionsCallback) {
|
||||
// Bail if the panel already exists
|
||||
if (id in _registeredPanels) {
|
||||
throw new Error("Home.panels: Panel already exists: id = " + id);
|
||||
}
|
||||
|
||||
if (!optionsCallback || typeof optionsCallback !== "function") {
|
||||
throw new Error(
|
||||
"Home.panels: Panel callback must be a function: id = " + id
|
||||
);
|
||||
}
|
||||
|
||||
_registeredPanels[id] = optionsCallback;
|
||||
},
|
||||
|
||||
unregister: function(id) {
|
||||
_assertPanelExists(id);
|
||||
|
||||
delete _registeredPanels[id];
|
||||
},
|
||||
|
||||
install: function(id) {
|
||||
_assertPanelExists(id);
|
||||
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "HomePanels:Install",
|
||||
panel: _generatePanel(id),
|
||||
});
|
||||
},
|
||||
|
||||
uninstall: function(id) {
|
||||
_assertPanelExists(id);
|
||||
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "HomePanels:Uninstall",
|
||||
id: id,
|
||||
});
|
||||
},
|
||||
|
||||
update: function(id) {
|
||||
_assertPanelExists(id);
|
||||
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "HomePanels:Update",
|
||||
panel: _generatePanel(id),
|
||||
});
|
||||
},
|
||||
|
||||
setAuthenticated: function(id, isAuthenticated) {
|
||||
_assertPanelExists(id);
|
||||
|
||||
let authKey = PREFS_PANEL_AUTH_PREFIX + id;
|
||||
let sharedPrefs = SharedPreferences.forProfile();
|
||||
sharedPrefs.setBoolPref(authKey, isAuthenticated);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
// Public API
|
||||
var Home = Object.freeze({
|
||||
banner: HomeBanner,
|
||||
panels: HomePanels,
|
||||
|
||||
// Lazy notification observer registered in browser.js
|
||||
onEvent: function(event, data, callback) {
|
||||
if (event in HomeBannerMessageHandlers) {
|
||||
HomeBannerMessageHandlers[event](data);
|
||||
} else if (event in HomePanelsMessageHandlers) {
|
||||
HomePanelsMessageHandlers[event](data);
|
||||
} else {
|
||||
Cu.reportError(
|
||||
"Home.observe: message handler not found for event: " + event
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,431 +0,0 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["HomeProvider"];
|
||||
|
||||
const { EventDispatcher } = ChromeUtils.import(
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { Sqlite } = ChromeUtils.import("resource://gre/modules/Sqlite.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
/*
|
||||
* SCHEMA_VERSION history:
|
||||
* 1: Create HomeProvider (bug 942288)
|
||||
* 2: Add filter column to items table (bug 942295/975841)
|
||||
* 3: Add background_color and background_url columns (bug 1157539)
|
||||
*/
|
||||
const SCHEMA_VERSION = 3;
|
||||
|
||||
// The maximum number of items you can attempt to save at once.
|
||||
const MAX_SAVE_COUNT = 100;
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
|
||||
});
|
||||
|
||||
const PREF_STORAGE_LAST_SYNC_TIME_PREFIX = "home.storage.lastSyncTime.";
|
||||
const PREF_SYNC_UPDATE_MODE = "home.sync.updateMode";
|
||||
const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
|
||||
return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
this,
|
||||
"gUpdateTimerManager",
|
||||
"@mozilla.org/updates/timer-manager;1",
|
||||
"nsIUpdateTimerManager"
|
||||
);
|
||||
|
||||
/**
|
||||
* All SQL statements should be defined here.
|
||||
*/
|
||||
const SQL = {
|
||||
createItemsTable:
|
||||
"CREATE TABLE items (" +
|
||||
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"dataset_id TEXT NOT NULL, " +
|
||||
"url TEXT," +
|
||||
"title TEXT," +
|
||||
"description TEXT," +
|
||||
"image_url TEXT," +
|
||||
"background_color TEXT," +
|
||||
"background_url TEXT," +
|
||||
"filter TEXT," +
|
||||
"created INTEGER" +
|
||||
")",
|
||||
|
||||
dropItemsTable: "DROP TABLE items",
|
||||
|
||||
insertItem:
|
||||
"INSERT INTO items (dataset_id, url, title, description, image_url, background_color, background_url, filter, created) " +
|
||||
"VALUES (:dataset_id, :url, :title, :description, :image_url, :background_color, :background_url, :filter, :created)",
|
||||
|
||||
deleteFromDataset: "DELETE FROM items WHERE dataset_id = :dataset_id",
|
||||
|
||||
addColumnBackgroundColor:
|
||||
"ALTER TABLE items ADD COLUMN background_color TEXT",
|
||||
|
||||
addColumnBackgroundUrl: "ALTER TABLE items ADD COLUMN background_url TEXT",
|
||||
};
|
||||
|
||||
/**
|
||||
* Technically this function checks to see if the user is on a local network,
|
||||
* but we express this as "wifi" to the user.
|
||||
*/
|
||||
function isUsingWifi() {
|
||||
let network = Cc["@mozilla.org/network/network-link-service;1"].getService(
|
||||
Ci.nsINetworkLinkService
|
||||
);
|
||||
return (
|
||||
network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_WIFI ||
|
||||
network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET
|
||||
);
|
||||
}
|
||||
|
||||
function getNowInSeconds() {
|
||||
return Math.round(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function getLastSyncPrefName(datasetId) {
|
||||
return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
|
||||
}
|
||||
|
||||
// Whether or not we've registered an update timer.
|
||||
var gTimerRegistered = false;
|
||||
|
||||
// Map of datasetId -> { interval: <integer>, callback: <function> }
|
||||
var gSyncCallbacks = {};
|
||||
|
||||
/**
|
||||
* nsITimerCallback implementation. Checks to see if it's time to sync any registered datasets.
|
||||
*
|
||||
* @param timer The timer which has expired.
|
||||
*/
|
||||
function syncTimerCallback(timer) {
|
||||
for (let datasetId in gSyncCallbacks) {
|
||||
let lastSyncTime = Services.prefs.getIntPref(
|
||||
getLastSyncPrefName(datasetId),
|
||||
0
|
||||
);
|
||||
|
||||
let now = getNowInSeconds();
|
||||
let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
|
||||
|
||||
if (lastSyncTime < now - interval) {
|
||||
let success = HomeProvider.requestSync(datasetId, callback);
|
||||
if (success) {
|
||||
Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var HomeStorage = function(datasetId) {
|
||||
this.datasetId = datasetId;
|
||||
};
|
||||
|
||||
var ValidationError = function(message) {
|
||||
this.name = "ValidationError";
|
||||
this.message = message;
|
||||
};
|
||||
ValidationError.prototype = new Error();
|
||||
ValidationError.prototype.constructor = ValidationError;
|
||||
|
||||
var HomeProvider = Object.freeze({
|
||||
ValidationError: ValidationError,
|
||||
|
||||
/**
|
||||
* Returns a storage associated with a given dataset identifer.
|
||||
*
|
||||
* @param datasetId
|
||||
* (string) Unique identifier for the dataset.
|
||||
*
|
||||
* @return HomeStorage
|
||||
*/
|
||||
getStorage: function(datasetId) {
|
||||
return new HomeStorage(datasetId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if it's an appropriate time to sync.
|
||||
*
|
||||
* @param datasetId Unique identifier for the dataset to sync.
|
||||
* @param callback Function to call when it's time to sync, called with datasetId as a parameter.
|
||||
*
|
||||
* @return boolean Whether or not we were able to sync.
|
||||
*/
|
||||
requestSync: function(datasetId, callback) {
|
||||
// Make sure it's a good time to sync.
|
||||
if (
|
||||
Services.prefs.getIntPref(PREF_SYNC_UPDATE_MODE) === 1 &&
|
||||
!isUsingWifi()
|
||||
) {
|
||||
Cu.reportError(
|
||||
"HomeProvider: Failed to sync because device is not on a local network"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
callback(datasetId);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies that a sync should be requested for the given dataset and update interval.
|
||||
*
|
||||
* @param datasetId Unique identifier for the dataset to sync.
|
||||
* @param interval Update interval in seconds. By default, this is throttled to 3600 seconds (1 hour).
|
||||
* @param callback Function to call when it's time to sync, called with datasetId as a parameter.
|
||||
*/
|
||||
addPeriodicSync: function(datasetId, interval, callback) {
|
||||
// Warn developers if they're expecting more frequent notifications that we allow.
|
||||
if (interval < gSyncCheckIntervalSecs) {
|
||||
Cu.reportError(
|
||||
"HomeProvider: Warning for dataset " +
|
||||
datasetId +
|
||||
" : Sync notifications are throttled to " +
|
||||
gSyncCheckIntervalSecs +
|
||||
" seconds"
|
||||
);
|
||||
}
|
||||
|
||||
gSyncCallbacks[datasetId] = {
|
||||
interval: interval,
|
||||
callback: callback,
|
||||
};
|
||||
|
||||
if (!gTimerRegistered) {
|
||||
gUpdateTimerManager.registerTimer(
|
||||
"home-provider-sync-timer",
|
||||
syncTimerCallback,
|
||||
gSyncCheckIntervalSecs
|
||||
);
|
||||
gTimerRegistered = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a periodic sync timer.
|
||||
*
|
||||
* @param datasetId Dataset to sync.
|
||||
*/
|
||||
removePeriodicSync: function(datasetId) {
|
||||
delete gSyncCallbacks[datasetId];
|
||||
Services.prefs.clearUserPref(getLastSyncPrefName(datasetId));
|
||||
// You can't unregister a update timer, so we don't try to do that.
|
||||
},
|
||||
});
|
||||
|
||||
var gDatabaseEnsured = false;
|
||||
|
||||
/**
|
||||
* Creates the database schema.
|
||||
*/
|
||||
function createDatabase(db) {
|
||||
return db.execute(SQL.createItemsTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the database schema to a new version.
|
||||
*/
|
||||
async function upgradeDatabase(db, oldVersion, newVersion) {
|
||||
switch (oldVersion) {
|
||||
case 1:
|
||||
// Migration from v1 to latest:
|
||||
// Recreate the items table discarding any
|
||||
// existing data.
|
||||
await db.execute(SQL.dropItemsTable);
|
||||
await db.execute(SQL.createItemsTable);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Migration from v2 to latest:
|
||||
// Add new columns: background_color, background_url
|
||||
await db.execute(SQL.addColumnBackgroundColor);
|
||||
await db.execute(SQL.addColumnBackgroundUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a database connection and makes sure that the database schema version
|
||||
* is correct, performing migrations if necessary. Consumers should be sure
|
||||
* to close any database connections they open.
|
||||
*
|
||||
* @return Promise
|
||||
* @resolves Handle on an opened SQLite database.
|
||||
*/
|
||||
async function getDatabaseConnection() {
|
||||
let db = await Sqlite.openConnection({ path: DB_PATH });
|
||||
if (gDatabaseEnsured) {
|
||||
return db;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check to see if we need to perform any migrations.
|
||||
let dbVersion = parseInt(await db.getSchemaVersion());
|
||||
|
||||
// getSchemaVersion() returns a 0 int if the schema
|
||||
// version is undefined.
|
||||
if (dbVersion === 0) {
|
||||
await createDatabase(db);
|
||||
} else if (dbVersion < SCHEMA_VERSION) {
|
||||
await upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
await db.setSchemaVersion(SCHEMA_VERSION);
|
||||
} catch (e) {
|
||||
// Close the DB connection before passing the exception to the consumer.
|
||||
await db.close();
|
||||
throw e;
|
||||
}
|
||||
|
||||
gDatabaseEnsured = true;
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an item to be saved to the DB.
|
||||
*
|
||||
* @param item
|
||||
* (object) item object to be validated.
|
||||
*/
|
||||
function validateItem(datasetId, item) {
|
||||
if (!item.url) {
|
||||
throw new ValidationError(
|
||||
"HomeStorage: All rows must have an URL: datasetId = " + datasetId
|
||||
);
|
||||
}
|
||||
|
||||
if (!item.image_url && !item.title && !item.description) {
|
||||
throw new ValidationError(
|
||||
"HomeStorage: All rows must have at least an image URL, " +
|
||||
"or a title or a description: datasetId = " +
|
||||
datasetId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var gRefreshTimers = {};
|
||||
|
||||
/**
|
||||
* Sends a message to Java to refresh the given dataset. Delays sending
|
||||
* messages to avoid successive refreshes, which can result in flashing views.
|
||||
*/
|
||||
function refreshDataset(datasetId) {
|
||||
// Bail if there's already a refresh timer waiting to fire
|
||||
if (gRefreshTimers[datasetId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(
|
||||
function(timer) {
|
||||
delete gRefreshTimers[datasetId];
|
||||
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "HomePanels:RefreshDataset",
|
||||
datasetId: datasetId,
|
||||
});
|
||||
},
|
||||
100,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT
|
||||
);
|
||||
|
||||
gRefreshTimers[datasetId] = timer;
|
||||
}
|
||||
|
||||
HomeStorage.prototype = {
|
||||
/**
|
||||
* Saves data rows to the DB.
|
||||
*
|
||||
* @param data
|
||||
* An array of JS objects represnting row items to save.
|
||||
* Each object may have the following properties:
|
||||
* - url (string)
|
||||
* - title (string)
|
||||
* - description (string)
|
||||
* - image_url (string)
|
||||
* - filter (string)
|
||||
* @param options
|
||||
* A JS object holding additional cofiguration properties.
|
||||
* The following properties are currently supported:
|
||||
* - replace (boolean): Whether or not to replace existing items.
|
||||
*
|
||||
* @return Promise
|
||||
* @resolves When the operation has completed.
|
||||
*/
|
||||
async save(data, options) {
|
||||
if (data && data.length > MAX_SAVE_COUNT) {
|
||||
throw new Error(
|
||||
`save failed for dataset = ${this.datasetId}: ` +
|
||||
`you cannot save more than ${MAX_SAVE_COUNT} items at once`
|
||||
);
|
||||
}
|
||||
|
||||
let db = await getDatabaseConnection();
|
||||
try {
|
||||
await db.executeTransaction(
|
||||
async function save_transaction() {
|
||||
if (options && options.replace) {
|
||||
await db.executeCached(SQL.deleteFromDataset, {
|
||||
dataset_id: this.datasetId,
|
||||
});
|
||||
}
|
||||
|
||||
// Insert data into DB.
|
||||
for (let item of data) {
|
||||
validateItem(this.datasetId, item);
|
||||
|
||||
// XXX: Directly pass item as params? More validation for item?
|
||||
let params = {
|
||||
dataset_id: this.datasetId,
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
image_url: item.image_url,
|
||||
background_color: item.background_color,
|
||||
background_url: item.background_url,
|
||||
filter: item.filter,
|
||||
created: Date.now(),
|
||||
};
|
||||
await db.executeCached(SQL.insertItem, params);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
refreshDataset(this.datasetId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes all rows associated with this storage.
|
||||
*
|
||||
* @return Promise
|
||||
* @resolves When the operation has completed.
|
||||
*/
|
||||
async deleteAll() {
|
||||
let db = await getDatabaseConnection();
|
||||
try {
|
||||
let params = { dataset_id: this.datasetId };
|
||||
await db.executeCached(SQL.deleteFromDataset, params);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
refreshDataset(this.datasetId);
|
||||
},
|
||||
};
|
|
@ -1,86 +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/. */
|
||||
|
||||
var EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { LightweightThemeManager } = ChromeUtils.import(
|
||||
"resource://gre/modules/LightweightThemeManager.jsm"
|
||||
);
|
||||
const { ExtensionUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/ExtensionUtils.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"EventDispatcher",
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
|
||||
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
|
||||
|
||||
let RESOLVE_PROPERTIES = ["headerURL"];
|
||||
|
||||
let handlers = new ExtensionUtils.DefaultMap(proto => {
|
||||
try {
|
||||
return Cc[`@mozilla.org/network/protocol;1?name=${proto}`].getService(
|
||||
Ci.nsISubstitutingProtocolHandler
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// The Java front-end code cannot understand internal protocols like
|
||||
// resource:, so resolve them to their underlying file: or jar: URIs
|
||||
// when possible.
|
||||
function maybeResolveURL(url) {
|
||||
try {
|
||||
let uri = Services.io.newURI(url);
|
||||
let handler = handlers.get(uri.scheme);
|
||||
if (handler) {
|
||||
return handler.resolveURI(uri);
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
class LightweightThemeConsumer {
|
||||
constructor(aDocument) {
|
||||
this._doc = aDocument;
|
||||
Services.obs.addObserver(this, "lightweight-theme-styling-update");
|
||||
|
||||
this._update(LightweightThemeManager.currentThemeWithFallback);
|
||||
}
|
||||
|
||||
observe(aSubject, aTopic, aData) {
|
||||
if (aTopic == "lightweight-theme-styling-update") {
|
||||
this._update(aSubject.wrappedJSObject.theme);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Services.obs.removeObserver(this, "lightweight-theme-styling-update");
|
||||
this._doc = null;
|
||||
}
|
||||
|
||||
_update(aData) {
|
||||
let active = aData && aData.id !== DEFAULT_THEME_ID;
|
||||
let msg = {
|
||||
type: active ? "LightweightTheme:Update" : "LightweightTheme:Disable",
|
||||
};
|
||||
|
||||
if (active) {
|
||||
msg.data = { ...aData };
|
||||
for (let prop of RESOLVE_PROPERTIES) {
|
||||
if (msg.data[prop]) {
|
||||
msg.data[prop] = maybeResolveURL(msg.data[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
EventDispatcher.instance.sendRequest(msg);
|
||||
}
|
||||
}
|
|
@ -1,81 +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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Snackbars"];
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"EventDispatcher",
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
|
||||
const LENGTH_INDEFINITE = -2;
|
||||
const LENGTH_LONG = 0;
|
||||
const LENGTH_SHORT = -1;
|
||||
|
||||
var Snackbars = {
|
||||
LENGTH_INDEFINITE: LENGTH_INDEFINITE,
|
||||
LENGTH_LONG: LENGTH_LONG,
|
||||
LENGTH_SHORT: LENGTH_SHORT,
|
||||
|
||||
show: function(aMessage, aDuration, aOptions) {
|
||||
// Takes care of the deprecated toast calls
|
||||
if (typeof aDuration === "string") {
|
||||
[aDuration, aOptions] = migrateToastIfNeeded(aDuration, aOptions);
|
||||
}
|
||||
|
||||
let msg = {
|
||||
type: "Snackbar:Show",
|
||||
message: aMessage,
|
||||
duration: aDuration,
|
||||
};
|
||||
|
||||
if (aOptions && aOptions.backgroundColor) {
|
||||
msg.backgroundColor = aOptions.backgroundColor;
|
||||
}
|
||||
|
||||
if (aOptions && aOptions.action) {
|
||||
msg.action = {};
|
||||
|
||||
if (aOptions.action.label) {
|
||||
msg.action.label = aOptions.action.label;
|
||||
}
|
||||
|
||||
EventDispatcher.instance
|
||||
.sendRequestForResult(msg)
|
||||
.then(result => aOptions.action.callback())
|
||||
.catch(result => {
|
||||
if (aOptions.action.rejection) {
|
||||
aOptions.action.rejection(result);
|
||||
} else if (result === null) {
|
||||
/* The snackbar was dismissed without executing the callback, nothing to do here. */
|
||||
} else {
|
||||
Cu.reportError(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
EventDispatcher.instance.sendRequest(msg);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function migrateToastIfNeeded(aDuration, aOptions) {
|
||||
let duration;
|
||||
if (aDuration === "long") {
|
||||
duration = LENGTH_LONG;
|
||||
} else {
|
||||
duration = LENGTH_SHORT;
|
||||
}
|
||||
|
||||
let options = {};
|
||||
if (aOptions && aOptions.button) {
|
||||
options.action = {
|
||||
label: aOptions.button.label,
|
||||
callback: () => aOptions.button.callback(),
|
||||
};
|
||||
}
|
||||
return [duration, options];
|
||||
}
|
|
@ -1,527 +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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["WebsiteMetadata"];
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"EventDispatcher",
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
|
||||
var WebsiteMetadata = {
|
||||
/**
|
||||
* Asynchronously parse the document extract metadata. A 'Website:Metadata' event with the metadata
|
||||
* will be sent.
|
||||
*/
|
||||
parseAsynchronously: function(doc) {
|
||||
let metadata = getMetadata(doc, doc.location.href, {
|
||||
image_url: metadataRules.image_url,
|
||||
provider: metadataRules.provider,
|
||||
description_length: metadataRules.description_length,
|
||||
});
|
||||
|
||||
// No metadata was extracted, so don't bother sending it.
|
||||
if (Object.keys(metadata).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = {
|
||||
type: "Website:Metadata",
|
||||
location: doc.location.href,
|
||||
hasImage: metadata.image_url && metadata.image_url !== "",
|
||||
metadata: JSON.stringify(metadata),
|
||||
};
|
||||
|
||||
EventDispatcher.instance.sendRequest(msg);
|
||||
},
|
||||
};
|
||||
|
||||
// #################################################################################################
|
||||
// # Modified version of makeUrlAbsolute() to not import url parser library (and dependencies)
|
||||
// #################################################################################################
|
||||
|
||||
function makeUrlAbsolute(context, relative) {
|
||||
var a = context.doc.createElement("a");
|
||||
a.href = relative;
|
||||
return a.href;
|
||||
}
|
||||
|
||||
// #################################################################################################
|
||||
// # page-metadata-parser
|
||||
// # https://github.com/mozilla/page-metadata-parser/
|
||||
// # 61c58cbd0f0bf2153df832a388a79c66b288b98c
|
||||
// #################################################################################################
|
||||
|
||||
function buildRuleset(name, rules, processors) {
|
||||
const reversedRules = Array.from(rules).reverse();
|
||||
const builtRuleset = ruleset(
|
||||
...reversedRules.map(([query, handler], order) =>
|
||||
rule(dom(query), node => [
|
||||
{
|
||||
score: order,
|
||||
flavor: name,
|
||||
notes: handler(node),
|
||||
},
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
return (doc, context) => {
|
||||
const kb = builtRuleset.score(doc);
|
||||
const maxNode = kb.max(name);
|
||||
|
||||
if (maxNode) {
|
||||
let value = maxNode.flavors.get(name);
|
||||
|
||||
if (processors) {
|
||||
processors.forEach(processor => {
|
||||
value = processor(value, context);
|
||||
});
|
||||
}
|
||||
|
||||
if (value) {
|
||||
if (value.trim) {
|
||||
return value.trim();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const descriptionRules = [
|
||||
[
|
||||
'meta[property="og:description"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
['meta[name="description"]', node => node.element.getAttribute("content")],
|
||||
];
|
||||
|
||||
const metadataRules = {
|
||||
description: {
|
||||
rules: descriptionRules,
|
||||
},
|
||||
|
||||
description_length: {
|
||||
rules: descriptionRules,
|
||||
processors: [description => description.length],
|
||||
},
|
||||
|
||||
icon_url: {
|
||||
rules: [
|
||||
[
|
||||
'link[rel="apple-touch-icon"]',
|
||||
node => node.element.getAttribute("href"),
|
||||
],
|
||||
[
|
||||
'link[rel="apple-touch-icon-precomposed"]',
|
||||
node => node.element.getAttribute("href"),
|
||||
],
|
||||
['link[rel="icon"]', node => node.element.getAttribute("href")],
|
||||
['link[rel="fluid-icon"]', node => node.element.getAttribute("href")],
|
||||
['link[rel="shortcut icon"]', node => node.element.getAttribute("href")],
|
||||
['link[rel="Shortcut Icon"]', node => node.element.getAttribute("href")],
|
||||
['link[rel="mask-icon"]', node => node.element.getAttribute("href")],
|
||||
],
|
||||
processors: [(icon_url, context) => makeUrlAbsolute(context, icon_url)],
|
||||
},
|
||||
|
||||
image_url: {
|
||||
rules: [
|
||||
[
|
||||
'meta[property="og:image:secure_url"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
[
|
||||
'meta[property="og:image:url"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
[
|
||||
'meta[property="og:image"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
[
|
||||
'meta[property="twitter:image"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
['meta[name="thumbnail"]', node => node.element.getAttribute("content")],
|
||||
],
|
||||
processors: [(image_url, context) => makeUrlAbsolute(context, image_url)],
|
||||
},
|
||||
|
||||
keywords: {
|
||||
rules: [
|
||||
['meta[name="keywords"]', node => node.element.getAttribute("content")],
|
||||
],
|
||||
processors: [
|
||||
keywords => keywords.split(",").map(keyword => keyword.trim()),
|
||||
],
|
||||
},
|
||||
|
||||
title: {
|
||||
rules: [
|
||||
[
|
||||
'meta[property="og:title"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
[
|
||||
'meta[property="twitter:title"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
['meta[name="hdl"]', node => node.element.getAttribute("content")],
|
||||
["title", node => node.element.text],
|
||||
],
|
||||
},
|
||||
|
||||
type: {
|
||||
rules: [
|
||||
[
|
||||
'meta[property="og:type"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
url: {
|
||||
rules: [
|
||||
['meta[property="og:url"]', node => node.element.getAttribute("content")],
|
||||
['link[rel="canonical"]', node => node.element.getAttribute("href")],
|
||||
],
|
||||
},
|
||||
|
||||
provider: {
|
||||
rules: [
|
||||
[
|
||||
'meta[property="og:site_name"]',
|
||||
node => node.element.getAttribute("content"),
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getMetadata(doc, url, rules) {
|
||||
const metadata = {};
|
||||
const context = { url, doc };
|
||||
const ruleSet = rules || metadataRules;
|
||||
|
||||
Object.keys(ruleSet).map(metadataKey => {
|
||||
const metadataRule = ruleSet[metadataKey];
|
||||
|
||||
if (Array.isArray(metadataRule.rules)) {
|
||||
const builtRule = buildRuleset(
|
||||
metadataKey,
|
||||
metadataRule.rules,
|
||||
metadataRule.processors
|
||||
);
|
||||
metadata[metadataKey] = builtRule(doc, context);
|
||||
} else {
|
||||
metadata[metadataKey] = getMetadata(doc, url, metadataRule);
|
||||
}
|
||||
});
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// #################################################################################################
|
||||
// # Fathom dependencies resolved
|
||||
// #################################################################################################
|
||||
|
||||
// const {forEach} = require('wu');
|
||||
function forEach(fn, obj) {
|
||||
for (let x of obj) {
|
||||
fn(x);
|
||||
}
|
||||
}
|
||||
|
||||
function best(iterable, by, isBetter) {
|
||||
let bestSoFar, bestKeySoFar;
|
||||
let isFirst = true;
|
||||
forEach(function(item) {
|
||||
const key = by(item);
|
||||
if (isBetter(key, bestKeySoFar) || isFirst) {
|
||||
bestSoFar = item;
|
||||
bestKeySoFar = key;
|
||||
isFirst = false;
|
||||
}
|
||||
}, iterable);
|
||||
if (isFirst) {
|
||||
throw new Error("Tried to call best() on empty iterable");
|
||||
}
|
||||
return bestSoFar;
|
||||
}
|
||||
|
||||
// const {max} = require('./utils');
|
||||
function max(iterable, by = identity) {
|
||||
return best(iterable, by, (a, b) => a > b);
|
||||
}
|
||||
|
||||
// #################################################################################################
|
||||
// # Fathom
|
||||
// # https://github.com/mozilla/fathom
|
||||
// # cac59e470816f17fc1efd4a34437b585e3e451cd
|
||||
// #################################################################################################
|
||||
|
||||
// Get a key of a map, first setting it to a default value if it's missing.
|
||||
function getDefault(map, key, defaultMaker) {
|
||||
if (map.has(key)) {
|
||||
return map.get(key);
|
||||
}
|
||||
const defaultValue = defaultMaker();
|
||||
map.set(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Construct a filtration network of rules.
|
||||
function ruleset(...rules) {
|
||||
const rulesByInputFlavor = new Map(); // [someInputFlavor: [rule, ...]]
|
||||
|
||||
// File each rule under its input flavor:
|
||||
forEach(
|
||||
rule =>
|
||||
getDefault(rulesByInputFlavor, rule.source.inputFlavor, () => []).push(
|
||||
rule
|
||||
),
|
||||
rules
|
||||
);
|
||||
|
||||
return {
|
||||
// Iterate over a DOM tree or subtree, building up a knowledgebase, a
|
||||
// data structure holding scores and annotations for interesting
|
||||
// elements. Return the knowledgebase.
|
||||
//
|
||||
// This is the "rank" portion of the rank-and-yank algorithm.
|
||||
score: function(tree) {
|
||||
const kb = knowledgebase();
|
||||
|
||||
// Introduce the whole DOM into the KB as flavor 'dom' to get
|
||||
// things started:
|
||||
const nonterminals = [[{ tree }, "dom"]]; // [[node, flavor], [node, flavor], ...]
|
||||
|
||||
// While there are new facts, run the applicable rules over them to
|
||||
// generate even newer facts. Repeat until everything's fully
|
||||
// digested. Rules run in no particular guaranteed order.
|
||||
while (nonterminals.length) {
|
||||
const [inNode, inFlavor] = nonterminals.pop();
|
||||
for (let rule of getDefault(rulesByInputFlavor, inFlavor, () => [])) {
|
||||
const outFacts = resultsOf(rule, inNode, inFlavor, kb);
|
||||
for (let fact of outFacts) {
|
||||
const outNode = kb.nodeForElement(fact.element);
|
||||
|
||||
// No matter whether or not this flavor has been
|
||||
// emitted before for this node, we multiply the score.
|
||||
// We want to be able to add rules that refine the
|
||||
// scoring of a node, without having to rewire the path
|
||||
// of flavors that winds through the ruleset.
|
||||
//
|
||||
// 1 score per Node is plenty. That simplifies our
|
||||
// data, our rankers, our flavor system (since we don't
|
||||
// need to represent score axes), and our engine. If
|
||||
// somebody wants more score axes, they can fake it
|
||||
// themselves with notes, thus paying only for what
|
||||
// they eat. (We can even provide functions that help
|
||||
// with that.) Most rulesets will probably be concerned
|
||||
// with scoring only 1 thing at a time anyway. So,
|
||||
// rankers return a score multiplier + 0 or more new
|
||||
// flavors with optional notes. Facts can never be
|
||||
// deleted from the KB by rankers (or order would start
|
||||
// to matter); after all, they're *facts*.
|
||||
outNode.score *= fact.score;
|
||||
|
||||
// Add a new annotation to a node--but only if there
|
||||
// wasn't already one of the given flavor already
|
||||
// there; otherwise there's no point.
|
||||
//
|
||||
// You might argue that we might want to modify an
|
||||
// existing note here, but that would be a bad
|
||||
// idea. Notes of a given flavor should be
|
||||
// considered immutable once laid down. Otherwise, the
|
||||
// order of execution of same-flavored rules could
|
||||
// matter, hurting pluggability. Emit a new flavor and
|
||||
// a new note if you want to do that.
|
||||
//
|
||||
// Also, choosing not to add a new fact to nonterminals
|
||||
// when we're not adding a new flavor saves the work of
|
||||
// running the rules against it, which would be
|
||||
// entirely redundant and perform no new work (unless
|
||||
// the rankers were nondeterministic, but don't do
|
||||
// that).
|
||||
if (!outNode.flavors.has(fact.flavor)) {
|
||||
outNode.flavors.set(fact.flavor, fact.notes);
|
||||
kb.indexNodeByFlavor(outNode, fact.flavor); // TODO: better encapsulation rather than indexing explicitly
|
||||
nonterminals.push([outNode, fact.flavor]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return kb;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Construct a container for storing and querying facts, where a fact has a
|
||||
// flavor (used to dispatch further rules upon), a corresponding DOM element, a
|
||||
// score, and some other arbitrary notes opaque to fathom.
|
||||
function knowledgebase() {
|
||||
const nodesByFlavor = new Map(); // Map{'texty' -> [NodeA],
|
||||
// 'spiffy' -> [NodeA, NodeB]}
|
||||
// NodeA = {element: <someElement>,
|
||||
//
|
||||
// // Global nodewide score. Add
|
||||
// // custom ones with notes if
|
||||
// // you want.
|
||||
// score: 8,
|
||||
//
|
||||
// // Flavors is a map of flavor names to notes:
|
||||
// flavors: Map{'texty' -> {ownText: 'blah',
|
||||
// someOtherNote: 'foo',
|
||||
// someCustomScore: 10},
|
||||
// // This is an empty note:
|
||||
// 'fluffy' -> undefined}}
|
||||
const nodesByElement = new Map();
|
||||
|
||||
return {
|
||||
// Return the "node" (our own data structure that we control) that
|
||||
// corresponds to a given DOM element, creating one if necessary.
|
||||
nodeForElement: function(element) {
|
||||
return getDefault(nodesByElement, element, () => ({
|
||||
element,
|
||||
score: 1,
|
||||
flavors: new Map(),
|
||||
}));
|
||||
},
|
||||
|
||||
// Return the highest-scored node of the given flavor, undefined if
|
||||
// there is none.
|
||||
max: function(flavor) {
|
||||
const nodes = nodesByFlavor.get(flavor);
|
||||
return nodes === undefined ? undefined : max(nodes, node => node.score);
|
||||
},
|
||||
|
||||
// Let the KB know that a new flavor has been added to an element.
|
||||
indexNodeByFlavor: function(node, flavor) {
|
||||
getDefault(nodesByFlavor, flavor, () => []).push(node);
|
||||
},
|
||||
|
||||
nodesOfFlavor: function(flavor) {
|
||||
return getDefault(nodesByFlavor, flavor, () => []);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Apply a rule (as returned by a call to rule()) to a fact, and return the
|
||||
// new facts that result.
|
||||
function resultsOf(rule, node, flavor, kb) {
|
||||
// If more types of rule pop up someday, do fancier dispatching here.
|
||||
return rule.source.flavor === "flavor"
|
||||
? resultsOfFlavorRule(rule, node, flavor)
|
||||
: resultsOfDomRule(rule, node, kb);
|
||||
}
|
||||
|
||||
// Pull the DOM tree off the special property of the root "dom" fact, and query
|
||||
// against it.
|
||||
function* resultsOfDomRule(rule, specialDomNode, kb) {
|
||||
// Use the special "tree" property of the special starting node:
|
||||
const matches = specialDomNode.tree.querySelectorAll(rule.source.selector);
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
// matches is a NodeList, which doesn't conform to iterator protocol
|
||||
const element = matches[i];
|
||||
const newFacts = explicitFacts(rule.ranker(kb.nodeForElement(element)));
|
||||
for (let fact of newFacts) {
|
||||
if (fact.element === undefined) {
|
||||
fact.element = element;
|
||||
}
|
||||
if (fact.flavor === undefined) {
|
||||
throw new Error(
|
||||
"Rankers of dom() rules must return a flavor in each fact. Otherwise, there is no way for that fact to be used later."
|
||||
);
|
||||
}
|
||||
yield fact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* resultsOfFlavorRule(rule, node, flavor) {
|
||||
const newFacts = explicitFacts(rule.ranker(node));
|
||||
|
||||
for (let fact of newFacts) {
|
||||
// If the ranker didn't specify a different element, assume it's
|
||||
// talking about the one we passed in:
|
||||
if (fact.element === undefined) {
|
||||
fact.element = node.element;
|
||||
}
|
||||
if (fact.flavor === undefined) {
|
||||
fact.flavor = flavor;
|
||||
}
|
||||
yield fact;
|
||||
}
|
||||
}
|
||||
|
||||
// Take the possibly abbreviated output of a ranker function, and make it
|
||||
// explicitly an iterable with a defined score.
|
||||
//
|
||||
// Rankers can return undefined, which means "no facts", a single fact, or an
|
||||
// array of facts.
|
||||
function* explicitFacts(rankerResult) {
|
||||
const array =
|
||||
rankerResult === undefined
|
||||
? []
|
||||
: Array.isArray(rankerResult)
|
||||
? rankerResult
|
||||
: [rankerResult];
|
||||
for (let fact of array) {
|
||||
if (fact.score === undefined) {
|
||||
fact.score = 1;
|
||||
}
|
||||
yield fact;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For the moment, a lot of responsibility is on the rankers to return a
|
||||
// pretty big data structure of up to 4 properties. This is a bit verbose for
|
||||
// an arrow function (as I hope we can use most of the time) and the usual case
|
||||
// will probably be returning just a score multiplier. Make that case more
|
||||
// concise.
|
||||
|
||||
// TODO: It is likely that rankers should receive the notes of their input type
|
||||
// as a 2nd arg, for brevity.
|
||||
|
||||
// Return a condition that uses a DOM selector to find its matches from the
|
||||
// original DOM tree.
|
||||
//
|
||||
// For consistency, Nodes will still be delivered to the transformers, but
|
||||
// they'll have empty flavors and score = 1.
|
||||
//
|
||||
// Condition constructors like dom() and flavor() build stupid, introspectable
|
||||
// objects that the query engine can read. They don't actually do the query
|
||||
// themselves. That way, the query planner can be smarter than them, figuring
|
||||
// out which indices to use based on all of them. (We'll probably keep a heap
|
||||
// by each dimension's score and a hash by flavor name, for starters.) Someday,
|
||||
// fancy things like this may be possible: rule(and(tag('p'), klass('snork')),
|
||||
// ...)
|
||||
function dom(selector) {
|
||||
return {
|
||||
flavor: "dom",
|
||||
inputFlavor: "dom",
|
||||
selector,
|
||||
};
|
||||
}
|
||||
|
||||
// Return a condition that discriminates on nodes of the knowledgebase by flavor.
|
||||
function flavor(inputFlavor) {
|
||||
return {
|
||||
flavor: "flavor",
|
||||
inputFlavor,
|
||||
};
|
||||
}
|
||||
|
||||
function rule(source, ranker) {
|
||||
return {
|
||||
source,
|
||||
ranker,
|
||||
};
|
||||
}
|
|
@ -6,21 +6,10 @@
|
|||
|
||||
# Most files are General, a few exceptions
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox for Android', 'General')
|
||||
|
||||
with Files('HomeProvider.jsm'):
|
||||
BUG_COMPONENT = ('Firefox for Android', 'Data Providers')
|
||||
|
||||
with Files('geckoview/**'):
|
||||
BUG_COMPONENT = ('GeckoView', 'General')
|
||||
|
||||
DIRS += ['geckoview']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'dbg-browser-actors.js',
|
||||
'Home.jsm',
|
||||
'HomeProvider.jsm',
|
||||
'LightweightThemeConsumer.jsm',
|
||||
'Snackbars.jsm',
|
||||
'WebsiteMetadata.jsm'
|
||||
]
|
||||
|
|
|
@ -33,7 +33,6 @@ skip-if = debug
|
|||
[test_desktop_useragent.html]
|
||||
[test_device_search_engine.html]
|
||||
[test_get_last_visited.html]
|
||||
[test_home_provider.html]
|
||||
[test_identity_mode.html]
|
||||
[test_media_playback.html]
|
||||
[test_migrate_ui.html]
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=942288
|
||||
Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for Bug 942288</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
|
||||
const {HomeProvider} = ChromeUtils.import("resource://gre/modules/HomeProvider.jsm");
|
||||
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {Sqlite} = ChromeUtils.import("resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
const TEST_DATASET_ID = "test-dataset-id";
|
||||
const TEST_URL = "http://test.com";
|
||||
const TEST_TITLE = "Test";
|
||||
const TEST_BACKGROUND_URL = "http://example.com/background";
|
||||
const TEST_BACKGROUND_COLOR = "#FF9500";
|
||||
|
||||
const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
|
||||
const TEST_INTERVAL_SECS = 1;
|
||||
|
||||
const DB_PATH = OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
|
||||
|
||||
test_request_sync();
|
||||
test_periodic_sync();
|
||||
|
||||
function test_request_sync() {
|
||||
// The current implementation of requestSync is synchronous.
|
||||
let success = HomeProvider.requestSync(TEST_DATASET_ID, function callback(datasetId) {
|
||||
is(datasetId, TEST_DATASET_ID, "expected dataset ID");
|
||||
});
|
||||
|
||||
ok(success, "requestSync success");
|
||||
}
|
||||
|
||||
function test_periodic_sync() {
|
||||
SimpleTest.registerCleanupFunction(function() {
|
||||
Services.prefs.clearUserPref(PREF_SYNC_CHECK_INTERVAL_SECS);
|
||||
HomeProvider.removePeriodicSync(TEST_DATASET_ID);
|
||||
});
|
||||
|
||||
// Lower the check interval for testing purposes.
|
||||
Services.prefs.setIntPref(PREF_SYNC_CHECK_INTERVAL_SECS, TEST_INTERVAL_SECS);
|
||||
|
||||
HomeProvider.addPeriodicSync(TEST_DATASET_ID, TEST_INTERVAL_SECS, function callback(datasetId) {
|
||||
is(datasetId, TEST_DATASET_ID, "expected dataset ID");
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function test_save_and_delete() {
|
||||
// Use the HomeProvider API to save some data.
|
||||
let storage = HomeProvider.getStorage(TEST_DATASET_ID);
|
||||
await storage.save([{
|
||||
title: TEST_TITLE,
|
||||
url: TEST_URL,
|
||||
background_url: TEST_BACKGROUND_URL,
|
||||
background_color: TEST_BACKGROUND_COLOR,
|
||||
}]);
|
||||
|
||||
// Peek in the DB to make sure we have the right data.
|
||||
let db = await Sqlite.openConnection({ path: DB_PATH });
|
||||
|
||||
// Make sure the items table was created.
|
||||
ok((await db.tableExists("items")), "items table exists");
|
||||
|
||||
// Make sure the correct values for the item ended up in there.
|
||||
let result = await db.execute("SELECT * FROM items", null, function onRow(row) {
|
||||
is(row.getResultByName("dataset_id"), TEST_DATASET_ID, "expected dataset ID");
|
||||
is(row.getResultByName("url"), TEST_URL, "expected test url");
|
||||
is(row.getResultByName("background_url"), TEST_BACKGROUND_URL, "expected background url");
|
||||
is(row.getResultByName("background_color"), TEST_BACKGROUND_COLOR, "expected background color");
|
||||
});
|
||||
|
||||
// Use the HomeProvider API to delete the data.
|
||||
await storage.deleteAll();
|
||||
|
||||
// Make sure the data was deleted.
|
||||
result = await db.execute("SELECT * FROM items");
|
||||
is(result.length, 0, "length is 0");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
add_task(async function test_row_validation() {
|
||||
// Use the HomeProvider API to save some data.
|
||||
let storage = HomeProvider.getStorage(TEST_DATASET_ID);
|
||||
|
||||
let invalidRows = [
|
||||
{ url: "url" },
|
||||
{ title: "title" },
|
||||
{ description: "description" },
|
||||
{ image_url: "image_url" },
|
||||
];
|
||||
|
||||
// None of these save calls should save anything
|
||||
for (let row of invalidRows) {
|
||||
try {
|
||||
await storage.save([row]);
|
||||
} catch (e) {
|
||||
// Just catch and ignore validation errors
|
||||
if (!(e instanceof HomeProvider.ValidationError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Peek in the DB to make sure we have the right data.
|
||||
let db = await Sqlite.openConnection({ path: DB_PATH });
|
||||
|
||||
// Make sure no data has been saved.
|
||||
let result = await db.execute("SELECT * FROM items");
|
||||
is(result.length, 0, "length is 0");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
add_task(async function test_save_transaction() {
|
||||
// Use the HomeProvider API to save some data.
|
||||
let storage = HomeProvider.getStorage(TEST_DATASET_ID);
|
||||
|
||||
// One valid, one invalid
|
||||
let rows = [
|
||||
{ title: TEST_TITLE, url: TEST_URL },
|
||||
{ image_url: "image_url" },
|
||||
];
|
||||
|
||||
// Try to save all the rows at once
|
||||
try {
|
||||
await storage.save(rows);
|
||||
} catch (e) {
|
||||
// Just catch and ignore validation errors
|
||||
if (!(e instanceof HomeProvider.ValidationError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Peek in the DB to make sure we have the right data.
|
||||
let db = await Sqlite.openConnection({ path: DB_PATH });
|
||||
|
||||
// Make sure no data has been saved.
|
||||
let result = await db.execute("SELECT * FROM items");
|
||||
is(result.length, 0, "length is 0");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942288">Mozilla Bug 942288</a>
|
||||
<br>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testHomeProvider</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче