Index: lib/filterStorage.js |
=================================================================== |
--- a/lib/filterStorage.js |
+++ b/lib/filterStorage.js |
@@ -12,775 +12,93 @@ |
* 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 <http://www.gnu.org/licenses/>. |
*/ |
"use strict"; |
-/** |
- * @fileOverview FilterStorage class responsible for managing user's |
- * subscriptions and filters. |
- */ |
+const {FilterStorage} = require("compiled"); |
+const {Subscription, SpecialSubscription} = require("subscriptionClasses"); |
+const {ActiveFilter} = require("filterClasses"); |
-const {IO} = require("io"); |
-const {Prefs} = require("prefs"); |
-const {Filter, ActiveFilter} = require("filterClasses"); |
-const {Subscription, SpecialSubscription, |
- ExternalSubscription} = require("subscriptionClasses"); |
-const {FilterNotifier} = require("filterNotifier"); |
- |
-/** |
- * Version number of the filter storage file format. |
- * @type {number} |
- */ |
-let formatVersion = 5; |
sergei
2017/08/24 13:32:07
I'm not sure that it's a good idea to already remo
Wladimir Palant
2017/08/31 11:32:36
I can leave it in of course, but that would be mer
|
+// Backwards compatibility |
+FilterStorage.getGroupForFilter = FilterStorage.getSubscriptionForFilter; |
/** |
- * This class reads user's filters from disk, manages them in memory |
- * and writes them back. |
- * @class |
+ * This property allows iterating over the list of subscriptions. It will delete |
+ * references automatically at the end of the current loop iteration. If you |
+ * need persistent references or element access by position you should use |
+ * FilterStorage.subscriptionAt() instead. |
+ * @type {Iterable} |
*/ |
-let FilterStorage = exports.FilterStorage = |
-{ |
- /** |
- * Will be set to true after the initial loadFromDisk() call completes. |
- * @type {boolean} |
- */ |
- initialized: false, |
- |
- /** |
- * Version number of the patterns.ini format used. |
- * @type {number} |
- */ |
- get formatVersion() |
- { |
- return formatVersion; |
- }, |
- |
- /** |
- * File containing the filter list |
- * @type {string} |
- */ |
- get sourceFile() |
+FilterStorage.subscriptions = { |
+ [Symbol.iterator]: function*() |
{ |
- return "patterns.ini"; |
- }, |
- |
- /** |
- * Will be set to true if no patterns.ini file exists. |
- * @type {boolean} |
- */ |
- firstRun: false, |
- |
- /** |
- * Map of properties listed in the filter storage file before the sections |
- * start. Right now this should be only the format version. |
- */ |
- fileProperties: Object.create(null), |
- |
- /** |
- * List of filter subscriptions containing all filters |
- * @type {Subscription[]} |
- */ |
- subscriptions: [], |
- |
- /** |
- * Map of subscriptions already on the list, by their URL/identifier |
- * @type {Object} |
- */ |
- knownSubscriptions: Object.create(null), |
- |
- /** |
- * Finds the filter group that a filter should be added to by default. Will |
- * return null if this group doesn't exist yet. |
- * @param {Filter} filter |
- * @return {?SpecialSubscription} |
- */ |
- getGroupForFilter(filter) |
- { |
- let generalSubscription = null; |
- for (let subscription of FilterStorage.subscriptions) |
+ for (let i = 0, l = FilterStorage.subscriptionCount; i < l; i++) |
{ |
- if (subscription instanceof SpecialSubscription && !subscription.disabled) |
+ let subscription = FilterStorage.subscriptionAt(i); |
+ try |
{ |
- // Always prefer specialized subscriptions |
- if (subscription.isDefaultFor(filter)) |
- return subscription; |
- |
- // If this is a general subscription - store it as fallback |
- if (!generalSubscription && |
- (!subscription.defaults || !subscription.defaults.length)) |
- { |
- generalSubscription = subscription; |
- } |
- } |
- } |
- return generalSubscription; |
- }, |
- |
- /** |
- * Adds a filter subscription to the list |
- * @param {Subscription} subscription filter subscription to be added |
- */ |
- addSubscription(subscription) |
- { |
- if (subscription.url in FilterStorage.knownSubscriptions) |
- return; |
- |
- FilterStorage.subscriptions.push(subscription); |
- FilterStorage.knownSubscriptions[subscription.url] = subscription; |
- addSubscriptionFilters(subscription); |
- |
- FilterNotifier.triggerListeners("subscription.added", subscription); |
- }, |
- |
- /** |
- * Removes a filter subscription from the list |
- * @param {Subscription} subscription filter subscription to be removed |
- */ |
- removeSubscription(subscription) |
- { |
- for (let i = 0; i < FilterStorage.subscriptions.length; i++) |
- { |
- if (FilterStorage.subscriptions[i].url == subscription.url) |
- { |
- removeSubscriptionFilters(subscription); |
- |
- FilterStorage.subscriptions.splice(i--, 1); |
- delete FilterStorage.knownSubscriptions[subscription.url]; |
- FilterNotifier.triggerListeners("subscription.removed", subscription); |
- return; |
+ yield subscription; |
} |
- } |
- }, |
- |
- /** |
- * Moves a subscription in the list to a new position. |
- * @param {Subscription} subscription filter subscription to be moved |
- * @param {Subscription} [insertBefore] filter subscription to insert before |
- * (if omitted the subscription will be put at the end of the list) |
- */ |
- moveSubscription(subscription, insertBefore) |
- { |
- let currentPos = FilterStorage.subscriptions.indexOf(subscription); |
- if (currentPos < 0) |
- return; |
- |
- let newPos = -1; |
- if (insertBefore) |
- newPos = FilterStorage.subscriptions.indexOf(insertBefore); |
- |
- if (newPos < 0) |
- newPos = FilterStorage.subscriptions.length; |
- |
- if (currentPos < newPos) |
- newPos--; |
- if (currentPos == newPos) |
- return; |
- |
- FilterStorage.subscriptions.splice(currentPos, 1); |
- FilterStorage.subscriptions.splice(newPos, 0, subscription); |
- FilterNotifier.triggerListeners("subscription.moved", subscription); |
- }, |
- |
- /** |
- * Replaces the list of filters in a subscription by a new list |
- * @param {Subscription} subscription filter subscription to be updated |
- * @param {Filter[]} filters new filter list |
- */ |
- updateSubscriptionFilters(subscription, filters) |
- { |
- removeSubscriptionFilters(subscription); |
- subscription.oldFilters = subscription.filters; |
- subscription.filters = filters; |
- addSubscriptionFilters(subscription); |
- FilterNotifier.triggerListeners("subscription.updated", subscription); |
- delete subscription.oldFilters; |
- }, |
- |
- /** |
- * Adds a user-defined filter to the list |
- * @param {Filter} filter |
- * @param {SpecialSubscription} [subscription] |
- * particular group that the filter should be added to |
- * @param {number} [position] |
- * position within the subscription at which the filter should be added |
- */ |
- addFilter(filter, subscription, position) |
- { |
- if (!subscription) |
- { |
- if (filter.subscriptions.some(s => s instanceof SpecialSubscription && |
- !s.disabled)) |
+ finally |
{ |
- return; // No need to add |
- } |
- subscription = FilterStorage.getGroupForFilter(filter); |
- } |
- if (!subscription) |
- { |
- // No group for this filter exists, create one |
- subscription = SpecialSubscription.createForFilter(filter); |
- this.addSubscription(subscription); |
- return; |
- } |
- |
- if (typeof position == "undefined") |
- position = subscription.filters.length; |
- |
- if (filter.subscriptions.indexOf(subscription) < 0) |
- filter.subscriptions.push(subscription); |
- subscription.filters.splice(position, 0, filter); |
- FilterNotifier.triggerListeners("filter.added", filter, subscription, |
- position); |
- }, |
- |
- /** |
- * Removes a user-defined filter from the list |
- * @param {Filter} filter |
- * @param {SpecialSubscription} [subscription] a particular filter group that |
- * the filter should be removed from (if ommited will be removed from all |
- * subscriptions) |
- * @param {number} [position] position inside the filter group at which the |
- * filter should be removed (if ommited all instances will be removed) |
- */ |
- removeFilter(filter, subscription, position) |
- { |
- let subscriptions = ( |
- subscription ? [subscription] : filter.subscriptions.slice() |
- ); |
- for (let i = 0; i < subscriptions.length; i++) |
- { |
- let currentSubscription = subscriptions[i]; |
- if (currentSubscription instanceof SpecialSubscription) |
- { |
- let positions = []; |
- if (typeof position == "undefined") |
- { |
- let index = -1; |
- do |
- { |
- index = currentSubscription.filters.indexOf(filter, index + 1); |
- if (index >= 0) |
- positions.push(index); |
- } while (index >= 0); |
- } |
- else |
- positions.push(position); |
- |
- for (let j = positions.length - 1; j >= 0; j--) |
- { |
- let currentPosition = positions[j]; |
- if (currentSubscription.filters[currentPosition] == filter) |
- { |
- currentSubscription.filters.splice(currentPosition, 1); |
- if (currentSubscription.filters.indexOf(filter) < 0) |
- { |
- let index = filter.subscriptions.indexOf(currentSubscription); |
- if (index >= 0) |
- filter.subscriptions.splice(index, 1); |
- } |
- FilterNotifier.triggerListeners( |
- "filter.removed", filter, currentSubscription, currentPosition |
- ); |
- } |
- } |
+ subscription.delete(); |
} |
} |
- }, |
- |
- /** |
- * Moves a user-defined filter to a new position |
- * @param {Filter} filter |
- * @param {SpecialSubscription} subscription filter group where the filter is |
- * located |
- * @param {number} oldPosition current position of the filter |
- * @param {number} newPosition new position of the filter |
- */ |
- moveFilter(filter, subscription, oldPosition, newPosition) |
- { |
- if (!(subscription instanceof SpecialSubscription) || |
- subscription.filters[oldPosition] != filter) |
- { |
- return; |
- } |
- |
- newPosition = Math.min(Math.max(newPosition, 0), |
- subscription.filters.length - 1); |
- if (oldPosition == newPosition) |
- return; |
- |
- subscription.filters.splice(oldPosition, 1); |
- subscription.filters.splice(newPosition, 0, filter); |
- FilterNotifier.triggerListeners("filter.moved", filter, subscription, |
- oldPosition, newPosition); |
- }, |
- |
- /** |
- * Increases the hit count for a filter by one |
- * @param {Filter} filter |
- */ |
- increaseHitCount(filter) |
- { |
- if (!Prefs.savestats || !(filter instanceof ActiveFilter)) |
- return; |
- |
- filter.hitCount++; |
- filter.lastHit = Date.now(); |
- }, |
- |
- /** |
- * Resets hit count for some filters |
- * @param {Filter[]} filters filters to be reset, if null all filters will |
- * be reset |
- */ |
- resetHitCounts(filters) |
- { |
- if (!filters) |
- { |
- filters = []; |
- for (let text in Filter.knownFilters) |
- filters.push(Filter.knownFilters[text]); |
- } |
- for (let filter of filters) |
- { |
- filter.hitCount = 0; |
- filter.lastHit = 0; |
- } |
- }, |
- |
- /** |
- * @callback TextSink |
- * @param {string?} line |
- */ |
- |
- /** |
- * Allows importing previously serialized filter data. |
- * @param {boolean} silent |
- * If true, no "load" notification will be sent out. |
- * @return {TextSink} |
- * Function to be called for each line of data. Calling it with null as |
- * parameter finalizes the import and replaces existing data. No changes |
- * will be applied before finalization, so import can be "aborted" by |
- * forgetting this callback. |
- */ |
- importData(silent) |
- { |
- let parser = new INIParser(); |
- return line => |
- { |
- parser.process(line); |
- if (line === null) |
- { |
- let knownSubscriptions = Object.create(null); |
- for (let subscription of parser.subscriptions) |
- knownSubscriptions[subscription.url] = subscription; |
- |
- this.fileProperties = parser.fileProperties; |
- this.subscriptions = parser.subscriptions; |
- this.knownSubscriptions = knownSubscriptions; |
- Filter.knownFilters = parser.knownFilters; |
- Subscription.knownSubscriptions = parser.knownSubscriptions; |
- |
- if (!silent) |
- FilterNotifier.triggerListeners("load"); |
- } |
- }; |
- }, |
- |
- /** |
- * Loads all subscriptions from the disk. |
- * @return {Promise} promise resolved or rejected when loading is complete |
- */ |
- loadFromDisk() |
- { |
- let tryBackup = backupIndex => |
- { |
- return this.restoreBackup(backupIndex, true).then(() => |
- { |
- if (this.subscriptions.length == 0) |
- return tryBackup(backupIndex + 1); |
- }).catch(error => |
- { |
- // Give up |
- }); |
- }; |
- |
- return IO.statFile(this.sourceFile).then(statData => |
- { |
- if (!statData.exists) |
- { |
- this.firstRun = true; |
- return; |
- } |
- |
- let parser = this.importData(true); |
- return IO.readFromFile(this.sourceFile, parser).then(() => |
- { |
- parser(null); |
- if (this.subscriptions.length == 0) |
- { |
- // No filter subscriptions in the file, this isn't right. |
- throw new Error("No data in the file"); |
- } |
- }); |
- }).catch(error => |
- { |
- Cu.reportError(error); |
- return tryBackup(1); |
- }).then(() => |
- { |
- this.initialized = true; |
- FilterNotifier.triggerListeners("load"); |
- }); |
- }, |
- |
- /** |
- * Constructs the file name for a patterns.ini backup. |
- * @param {number} backupIndex |
- * number of the backup file (1 being the most recent) |
- * @return {string} backup file name |
- */ |
- getBackupName(backupIndex) |
- { |
- let [name, extension] = this.sourceFile.split(".", 2); |
- return (name + "-backup" + backupIndex + "." + extension); |
- }, |
- |
- /** |
- * Restores an automatically created backup. |
- * @param {number} backupIndex |
- * number of the backup to restore (1 being the most recent) |
- * @param {boolean} silent |
- * If true, no "load" notification will be sent out. |
- * @return {Promise} promise resolved or rejected when restoring is complete |
- */ |
- restoreBackup(backupIndex, silent) |
- { |
- let backupFile = this.getBackupName(backupIndex); |
- let parser = this.importData(silent); |
- return IO.readFromFile(backupFile, parser).then(() => |
- { |
- parser(null); |
- return this.saveToDisk(); |
- }); |
- }, |
- |
- /** |
- * Generator serializing filter data and yielding it line by line. |
- */ |
- *exportData() |
- { |
- // Do not persist external subscriptions |
- let subscriptions = this.subscriptions.filter( |
- s => !(s instanceof ExternalSubscription) |
- ); |
- |
- yield "# Adblock Plus preferences"; |
- yield "version=" + formatVersion; |
- |
- let saved = new Set(); |
- let buf = []; |
- |
- // Save subscriptions |
- for (let subscription of subscriptions) |
- { |
- yield ""; |
- |
- subscription.serialize(buf); |
- if (subscription.filters.length) |
- { |
- buf.push("", "[Subscription filters]"); |
- subscription.serializeFilters(buf); |
- } |
- for (let line of buf) |
- yield line; |
- buf.splice(0); |
- } |
- |
- // Save filter data |
- for (let subscription of subscriptions) |
- { |
- for (let filter of subscription.filters) |
- { |
- if (!saved.has(filter.text)) |
- { |
- filter.serialize(buf); |
- saved.add(filter.text); |
- for (let line of buf) |
- yield line; |
- buf.splice(0); |
- } |
- } |
- } |
- }, |
- |
- /** |
- * Will be set to true if saveToDisk() is running (reentrance protection). |
- * @type {boolean} |
- */ |
- _saving: false, |
- |
- /** |
- * Will be set to true if a saveToDisk() call arrives while saveToDisk() is |
- * already running (delayed execution). |
- * @type {boolean} |
- */ |
- _needsSave: false, |
- |
- /** |
- * Saves all subscriptions back to disk |
- * @return {Promise} promise resolved or rejected when saving is complete |
- */ |
- saveToDisk() |
- { |
- if (this._saving) |
- { |
- this._needsSave = true; |
- return; |
- } |
- |
- this._saving = true; |
- |
- return Promise.resolve().then(() => |
- { |
- // First check whether we need to create a backup |
- if (Prefs.patternsbackups <= 0) |
- return false; |
- |
- return IO.statFile(this.sourceFile).then(statData => |
- { |
- if (!statData.exists) |
- return false; |
- |
- return IO.statFile(this.getBackupName(1)).then(backupStatData => |
- { |
- if (backupStatData.exists && |
- (Date.now() - backupStatData.lastModified) / 3600000 < |
- Prefs.patternsbackupinterval) |
- { |
- return false; |
- } |
- return true; |
- }); |
- }); |
- }).then(backupRequired => |
- { |
- if (!backupRequired) |
- return; |
- |
- let ignoreErrors = error => |
- { |
- // Expected error, backup file doesn't exist. |
- }; |
- |
- let renameBackup = index => |
- { |
- if (index > 0) |
- { |
- return IO.renameFile(this.getBackupName(index), |
- this.getBackupName(index + 1)) |
- .catch(ignoreErrors) |
- .then(() => renameBackup(index - 1)); |
- } |
- |
- return IO.renameFile(this.sourceFile, this.getBackupName(1)) |
- .catch(ignoreErrors); |
- }; |
- |
- // Rename existing files |
- return renameBackup(Prefs.patternsbackups - 1); |
- }).catch(error => |
- { |
- // Errors during backup creation shouldn't prevent writing filters. |
- Cu.reportError(error); |
- }).then(() => |
- { |
- return IO.writeToFile(this.sourceFile, this.exportData()); |
- }).then(() => |
- { |
- FilterNotifier.triggerListeners("save"); |
- }).catch(error => |
- { |
- // If saving failed, report error but continue - we still have to process |
- // flags. |
- Cu.reportError(error); |
- }).then(() => |
- { |
- this._saving = false; |
- if (this._needsSave) |
- { |
- this._needsSave = false; |
- this.saveToDisk(); |
- } |
- }); |
- }, |
- |
- /** |
- * @typedef FileInfo |
- * @type {object} |
- * @property {nsIFile} file |
- * @property {number} lastModified |
- */ |
- |
- /** |
- * Returns a promise resolving in a list of existing backup files. |
- * @return {Promise.<FileInfo[]>} |
- */ |
- getBackupFiles() |
- { |
- let backups = []; |
- |
- let checkBackupFile = index => |
- { |
- return IO.statFile(this.getBackupName(index)).then(statData => |
- { |
- if (!statData.exists) |
- return backups; |
- |
- backups.push({ |
- index, |
- lastModified: statData.lastModified |
- }); |
- return checkBackupFile(index + 1); |
- }).catch(error => |
- { |
- // Something went wrong, return whatever data we got so far. |
- Cu.reportError(error); |
- return backups; |
- }); |
- }; |
- |
- return checkBackupFile(1); |
} |
}; |
/** |
- * Joins subscription's filters to the subscription without any notifications. |
- * @param {Subscription} subscription |
- * filter subscription that should be connected to its filters |
+ * Adds a user-defined filter to the most suitable subscription in the list, |
+ * creates one if none found. |
+ * @param {Filter} filter |
*/ |
-function addSubscriptionFilters(subscription) |
+FilterStorage.addFilter = function(filter) |
{ |
- if (!(subscription.url in FilterStorage.knownSubscriptions)) |
- return; |
- |
- for (let filter of subscription.filters) |
- filter.subscriptions.push(subscription); |
-} |
+ for (let subscription of this.subscriptions) |
+ if (!subscription.disabled && subscription.indexOfFilter(filter) >= 0) |
+ return; |
-/** |
- * Removes subscription's filters from the subscription without any |
- * notifications. |
- * @param {Subscription} subscription filter subscription to be removed |
- */ |
-function removeSubscriptionFilters(subscription) |
-{ |
- if (!(subscription.url in FilterStorage.knownSubscriptions)) |
- return; |
- |
- for (let filter of subscription.filters) |
+ let subscription = this.getSubscriptionForFilter(filter); |
+ try |
{ |
- let i = filter.subscriptions.indexOf(subscription); |
- if (i >= 0) |
- filter.subscriptions.splice(i, 1); |
+ if (!subscription) |
+ { |
+ subscription = Subscription.fromURL(null); |
+ subscription.makeDefaultFor(filter); |
+ this.addSubscription(subscription); |
+ } |
+ subscription.insertFilterAt(filter, subscription.filterCount); |
} |
-} |
+ finally |
+ { |
+ if (subscription) |
+ subscription.delete(); |
+ } |
+ return true; |
+}; |
/** |
- * Listener returned by FilterStorage.importData(), parses filter data. |
- * @constructor |
+ * Removes a user-defined filter from the list |
+ * @param {Filter} filter |
*/ |
-function INIParser() |
-{ |
- this.fileProperties = this.curObj = {}; |
- this.subscriptions = []; |
- this.knownFilters = Object.create(null); |
- this.knownSubscriptions = Object.create(null); |
-} |
-INIParser.prototype = |
+FilterStorage.removeFilter = function(filter) |
{ |
- linesProcessed: 0, |
- subscriptions: null, |
- knownFilters: null, |
- knownSubscriptions: null, |
- wantObj: true, |
- fileProperties: null, |
- curObj: null, |
- curSection: null, |
- |
- process(val) |
+ for (let subscription of this.subscriptions) |
{ |
- let origKnownFilters = Filter.knownFilters; |
- Filter.knownFilters = this.knownFilters; |
- let origKnownSubscriptions = Subscription.knownSubscriptions; |
- Subscription.knownSubscriptions = this.knownSubscriptions; |
- let match; |
- try |
+ if (subscription instanceof SpecialSubscription) |
{ |
- if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val))) |
- this.curObj[match[1]] = match[2]; |
- else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val))) |
+ while (true) |
{ |
- if (this.curObj) |
- { |
- // Process current object before going to next section |
- switch (this.curSection) |
- { |
- case "filter": |
- if ("text" in this.curObj) |
- Filter.fromObject(this.curObj); |
- break; |
- case "subscription": { |
- let subscription = Subscription.fromObject(this.curObj); |
- if (subscription) |
- this.subscriptions.push(subscription); |
- break; |
- } |
- case "subscription filters": |
- if (this.subscriptions.length) |
- { |
- let subscription = this.subscriptions[ |
- this.subscriptions.length - 1 |
- ]; |
- for (let text of this.curObj) |
- { |
- let filter = Filter.fromText(text); |
- subscription.filters.push(filter); |
- filter.subscriptions.push(subscription); |
- } |
- } |
- break; |
- } |
- } |
- |
- if (val === null) |
- return; |
- |
- this.curSection = match[1].toLowerCase(); |
- switch (this.curSection) |
- { |
- case "filter": |
- case "subscription": |
- this.wantObj = true; |
- this.curObj = {}; |
- break; |
- case "subscription filters": |
- this.wantObj = false; |
- this.curObj = []; |
- break; |
- default: |
- this.wantObj = undefined; |
- this.curObj = null; |
- } |
+ let index = subscription.indexOfFilter(filter); |
+ if (index >= 0) |
+ subscription.removeFilterAt(index); |
+ else |
+ break; |
} |
- else if (this.wantObj === false && val) |
- this.curObj.push(val.replace(/\\\[/g, "[")); |
- } |
- finally |
- { |
- Filter.knownFilters = origKnownFilters; |
- Subscription.knownSubscriptions = origKnownSubscriptions; |
} |
} |
}; |
+ |
+exports.FilterStorage = FilterStorage; |