/* * This file is part of Adblock Plus , * Copyright (C) 2006-2015 Eyeo GmbH * * Adblock Plus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * Adblock Plus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Adblock Plus. If not, see . */ /** * @fileOverview Firefox Sync integration */ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); let {FilterStorage} = require("filterStorage"); let {FilterNotifier} = require("filterNotifier"); let {Synchronizer} = require("synchronizer"); let {Subscription, SpecialSubscription, DownloadableSubscription, ExternalSubscription} = require("subscriptionClasses"); let {Filter, ActiveFilter} = require("filterClasses"); // Firefox Sync classes are set later in initEngine() let Service, Engines, SyncEngine, Store, Tracker; /** * ID of the only record stored * @type String */ let filtersRecordID = "6fad6286-8207-46b6-aa39-8e0ce0bd7c49"; let Sync = exports.Sync = { /** * Will be set to true if/when Weave starts up. * @type Boolean */ initialized: false, /** * Whether Weave requested us to track changes. * @type Boolean */ trackingEnabled: false, /** * Returns Adblock Plus sync engine. * @result Engine */ getEngine: function() { if (this.initialized) return Engines.get("adblockplus"); else return null; } }; /** * Listens to notifications from Sync service. */ let SyncServiceObserver = { init: function() { try { let {Status, STATUS_DISABLED, CLIENT_NOT_CONFIGURED} = Cu.import("resource://services-sync/status.js", null); Sync.initialized = Status.ready; Sync.trackingEnabled = (Status.service != STATUS_DISABLED && Status.service != CLIENT_NOT_CONFIGURED); } catch (e) { return; } if (Sync.initialized) this.initEngine(); else Services.obs.addObserver(this, "weave:service:ready", true); Services.obs.addObserver(this, "weave:engine:start-tracking", true); Services.obs.addObserver(this, "weave:engine:stop-tracking", true); onShutdown.add(function() { try { Services.obs.removeObserver(this, "weave:service:ready"); } catch (e) {} Services.obs.removeObserver(this, "weave:engine:start-tracking"); Services.obs.removeObserver(this, "weave:engine:stop-tracking"); }.bind(this)); }, initEngine: function() { ({Engines, SyncEngine, Store, Tracker} = Cu.import("resource://services-sync/engines.js")); if (typeof Engines == "undefined") { ({Service} = Cu.import("resource://services-sync/service.js")); Engines = Service.engineManager; } ABPEngine.prototype.__proto__ = SyncEngine.prototype; ABPStore.prototype.__proto__ = Store.prototype; ABPTracker.prototype.__proto__ = Tracker.prototype; Engines.register(ABPEngine); onShutdown.add(function() { Engines.unregister("adblockplus"); }); }, observe: function(subject, topic, data) { switch (topic) { case "weave:service:ready": if (Sync.initialized) return; this.initEngine(); Sync.initialized = true; break; case "weave:engine:start-tracking": Sync.trackingEnabled = true; if (trackerInstance) trackerInstance.startTracking(); break; case "weave:engine:stop-tracking": Sync.trackingEnabled = false; if (trackerInstance) trackerInstance.stopTracking(); break; } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }; function ABPEngine() { SyncEngine.call(this, "AdblockPlus", Service); } ABPEngine.prototype = { _storeObj: ABPStore, _trackerObj: ABPTracker, version: 1, _reconcile: function(item) { // Always process server data, we will do the merging ourselves return true; } }; function ABPStore(name, engine) { Store.call(this, name, engine); } ABPStore.prototype = { getAllIDs: function() { let result = {} result[filtersRecordID] = true; return result; }, changeItemID: function(oldId, newId) { // This should not be called, our engine doesn't implement _findDupe throw Cr.NS_ERROR_UNEXPECTED; }, itemExists: function(id) { // Only one id exists so far return (id == filtersRecordID); }, createRecord: function(id, collection) { let record = new ABPEngine.prototype._recordObj(collection, id); if (id == filtersRecordID) { record.cleartext = { id: id, subscriptions: [], }; for (let subscription of FilterStorage.subscriptions) { if (subscription instanceof ExternalSubscription) continue; let subscriptionEntry = { url: subscription.url, disabled: subscription.disabled }; if (subscription instanceof SpecialSubscription) { subscriptionEntry.filters = []; for (let filter of subscription.filters) { let filterEntry = {text: filter.text}; if (filter instanceof ActiveFilter) filterEntry.disabled = filter.disabled; subscriptionEntry.filters.push(filterEntry); } } else subscriptionEntry.title = subscription.title; record.cleartext.subscriptions.push(subscriptionEntry); } // Data sent, forget about local changes now trackerInstance.clearPrivateChanges() } else record.deleted = true; return record; }, create: function(record) { // This should not be called because our record list doesn't change but // call update just in case. this.update(record); }, update: function(record) { if (record.id != filtersRecordID) return; this._log.trace("Merging in remote data"); let data = record.cleartext.subscriptions; // First make sure we have the same subscriptions on both sides let seenSubscription = Object.create(null); for (let remoteSubscription of data) { seenSubscription[remoteSubscription.url] = true; if (remoteSubscription.url in FilterStorage.knownSubscriptions) { let subscription = FilterStorage.knownSubscriptions[remoteSubscription.url]; if (!trackerInstance.didSubscriptionChange(remoteSubscription)) { // Only change local subscription if there were no changes, otherwise dismiss remote changes subscription.disabled = remoteSubscription.disabled; if (subscription instanceof DownloadableSubscription) subscription.title = remoteSubscription.title; } } else if (!trackerInstance.didSubscriptionChange(remoteSubscription)) { // Subscription was added remotely, add it locally as well let subscription = Subscription.fromURL(remoteSubscription.url); if (!subscription) continue; subscription.disabled = remoteSubscription.disabled; if (subscription instanceof DownloadableSubscription) { subscription.title = remoteSubscription.title; FilterStorage.addSubscription(subscription); Synchronizer.execute(subscription); } } } for (let subscription of FilterStorage.subscriptions.slice()) { if (!(subscription.url in seenSubscription) && subscription instanceof DownloadableSubscription && !trackerInstance.didSubscriptionChange(subscription)) { // Subscription was removed remotely, remove it locally as well FilterStorage.removeSubscription(subscription); } } // Now sync the custom filters let seenFilter = Object.create(null); for (let remoteSubscription of data) { if (!("filters" in remoteSubscription)) continue; for (let remoteFilter of remoteSubscription.filters) { seenFilter[remoteFilter.text] = true; let filter = Filter.fromText(remoteFilter.text); if (trackerInstance.didFilterChange(filter)) continue; if (filter.subscriptions.some((subscription) => subscription instanceof SpecialSubscription)) { // Filter might have been changed remotely if (filter instanceof ActiveFilter) filter.disabled = remoteFilter.disabled; } else { // Filter was added remotely, add it locally as well FilterStorage.addFilter(filter); } } } for (let subscription of FilterStorage.subscriptions) { if (!(subscription instanceof SpecialSubscription)) continue; for (let filter of subscription.filters.slice()) { if (!(filter.text in seenFilter) && !trackerInstance.didFilterChange(filter)) { // Filter was removed remotely, remove it locally as well FilterStorage.removeFilter(filter); } } } // Merge done, forget about local changes now trackerInstance.clearPrivateChanges() }, remove: function(record) { // Shouldn't be called but if it is - ignore }, wipe: function() { this._log.trace("Got wipe command, removing all data"); for (let subscription of FilterStorage.subscriptions.slice()) { if (subscription instanceof DownloadableSubscription) FilterStorage.removeSubscription(subscription); else if (subscription instanceof SpecialSubscription) { for (let filter of subscription.filters.slice()) FilterStorage.removeFilter(filter); } } // Data wiped, forget about local changes now trackerInstance.clearPrivateChanges() } }; /** * Hack to allow store to use the tracker - store tracker pointer globally. */ let trackerInstance = null; function ABPTracker(name, engine) { Tracker.call(this, name, engine); this.privateTracker = new Tracker(name + ".private", engine); trackerInstance = this; this.onChange = this.onChange.bind(this); if (Sync.trackingEnabled) this.startTracking(); } ABPTracker.prototype = { privateTracker: null, startTracking: function() { FilterNotifier.addListener(this.onChange); }, stopTracking: function() { FilterNotifier.removeListener(this.onChange); }, clearPrivateChanges: function() { this.privateTracker.clearChangedIDs(); }, addPrivateChange: function(id) { // Ignore changes during syncing if (this.ignoreAll) return; this.addChangedID(filtersRecordID); this.privateTracker.addChangedID(id); this.score += 10; }, didSubscriptionChange: function(subscription) { return ("subscription " + subscription.url) in this.privateTracker.changedIDs; }, didFilterChange: function(filter) { return ("filter " + filter.text) in this.privateTracker.changedIDs; }, onChange: function(action, item) { switch (action) { case "subscription.updated": if ("oldSubscription" in item) { // Subscription moved to a new address this.addPrivateChange("subscription " + item.url); this.addPrivateChange("subscription " + item.oldSubscription.url); } else if (item instanceof SpecialSubscription) { // User's filters changed via Preferences window for (let filter of item.filters) this.addPrivateChange("filter " + filter.text); for (let filter of item.oldFilters) this.addPrivateChange("filter " + filter.text); } break; case "subscription.added": case "subscription.removed": case "subscription.disabled": case "subscription.title": this.addPrivateChange("subscription " + item.url); break; case "filter.added": case "filter.removed": case "filter.disabled": this.addPrivateChange("filter " + item.text); break; } } }; SyncServiceObserver.init();