/*
* 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();