Index: lib/filterStorage.js |
=================================================================== |
--- a/lib/filterStorage.js |
+++ b/lib/filterStorage.js |
@@ -13,224 +13,250 @@ |
* |
* 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. |
+ * @fileOverview <code>filterStorage</code> object responsible for managing the |
+ * user's subscriptions and filters. |
*/ |
const {IO} = require("io"); |
const {Prefs} = require("prefs"); |
const {Filter, ActiveFilter} = require("./filterClasses"); |
const {Subscription, SpecialSubscription, |
ExternalSubscription} = require("./subscriptionClasses"); |
const {filterNotifier} = require("./filterNotifier"); |
const {INIParser} = require("./iniParser"); |
/** |
* Version number of the filter storage file format. |
* @type {number} |
*/ |
-let formatVersion = 5; |
+const FORMAT_VERSION = 5; |
/** |
- * This class reads user's filters from disk, manages them in memory |
- * and writes them back. |
- * @class |
+ * {@link filterStorage} implementation. |
*/ |
-let FilterStorage = exports.FilterStorage = |
+class FilterStorage |
{ |
/** |
- * Will be set to true after the initial loadFromDisk() call completes. |
- * @type {boolean} |
+ * @hideconstructor |
*/ |
- initialized: false, |
+ constructor() |
+ { |
+ /** |
+ * Will be set to true after the initial {@link FilterStorage#loadFromDisk} |
+ * call completes. |
+ * @type {boolean} |
+ */ |
+ this.initialized = false; |
+ |
+ /** |
+ * Will be set to <code>true</code> if no <code>patterns.ini</code> file |
+ * exists. |
+ * @type {boolean} |
+ */ |
+ this.firstRun = false; |
+ |
+ /** |
+ * Map of properties listed in the filter storage file before the sections |
+ * start. Right now this should be only the format version. |
+ * @type {object} |
+ */ |
+ this.fileProperties = Object.create(null); |
+ |
+ /** |
+ * Map of subscriptions already on the list, by their URL/identifier. |
+ * @type {Map.<string,Subscription>} |
+ */ |
+ this.knownSubscriptions = new Map(); |
+ |
+ /** |
+ * Will be set to true if {@link FilterStorage#saveToDisk} is running |
+ * (reentrance protection). |
+ * @type {boolean} |
+ * @private |
+ */ |
+ this._saving = false; |
+ |
+ /** |
+ * Will be set to true if a {@link FilterStorage#saveToDisk} call arrives |
+ * while {@link FilterStorage#saveToDisk} is already running (delayed |
+ * execution). |
+ * @type {boolean} |
+ * @private |
+ */ |
+ this._needsSave = false; |
+ } |
/** |
- * Version number of the patterns.ini format used. |
+ * The version number of the <code>patterns.ini</code> format used. |
* @type {number} |
*/ |
get formatVersion() |
{ |
- return formatVersion; |
- }, |
+ return FORMAT_VERSION; |
+ } |
/** |
- * File containing the filter list |
+ * The file containing the subscriptions. |
* @type {string} |
*/ |
get sourceFile() |
{ |
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), |
- |
- /** |
- * Yields subscriptions containing all filters |
+ * Yields all subscriptions in the storage. |
* @yields {Subscription} |
*/ |
*subscriptions() |
{ |
yield* this.knownSubscriptions.values(); |
- }, |
+ } |
/** |
- * Number of known subscriptions. |
+ * The number of subscriptions in the storage. |
* @type {number} |
*/ |
get subscriptionCount() |
{ |
return this.knownSubscriptions.size; |
- }, |
- |
- /** |
- * Map of subscriptions already on the list, by their URL/identifier |
- * @type {Map.<string,Subscription>} |
- */ |
- knownSubscriptions: new Map(), |
+ } |
/** |
* Finds the filter group that a filter should be added to by default. Will |
- * return null if this group doesn't exist yet. |
+ * return <code>null</code> if this group doesn't exist yet. |
* @param {Filter} filter |
- * @return {?SpecialSubscription} |
+ * @returns {?SpecialSubscription} |
*/ |
getGroupForFilter(filter) |
{ |
let generalSubscription = null; |
- for (let subscription of FilterStorage.knownSubscriptions.values()) |
+ for (let subscription of this.knownSubscriptions.values()) |
{ |
if (subscription instanceof SpecialSubscription && !subscription.disabled) |
{ |
// 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 |
+ * Adds a subscription to the storage. |
+ * @param {Subscription} subscription The subscription to be added. |
*/ |
addSubscription(subscription) |
{ |
- if (FilterStorage.knownSubscriptions.has(subscription.url)) |
+ if (this.knownSubscriptions.has(subscription.url)) |
return; |
- FilterStorage.knownSubscriptions.set(subscription.url, subscription); |
- addSubscriptionFilters(subscription); |
+ this.knownSubscriptions.set(subscription.url, subscription); |
+ connectSubscriptionFilters(subscription); |
filterNotifier.emit("subscription.added", subscription); |
- }, |
+ } |
/** |
- * Removes a filter subscription from the list |
- * @param {Subscription} subscription filter subscription to be removed |
+ * Removes a subscription from the storage. |
+ * @param {Subscription} subscription The subscription to be removed. |
*/ |
removeSubscription(subscription) |
{ |
- if (!FilterStorage.knownSubscriptions.has(subscription.url)) |
+ if (!this.knownSubscriptions.has(subscription.url)) |
return; |
- removeSubscriptionFilters(subscription); |
+ disconnectSubscriptionFilters(subscription); |
- FilterStorage.knownSubscriptions.delete(subscription.url); |
+ this.knownSubscriptions.delete(subscription.url); |
// This should be the last remaining reference to the Subscription |
// object. |
Subscription.knownSubscriptions.delete(subscription.url); |
filterNotifier.emit("subscription.removed", 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 |
+ * Replaces the list of filters in a subscription with a new list. |
+ * @param {Subscription} subscription The subscription to be updated. |
+ * @param {Array.<Filter>} filters The new list of filters. |
*/ |
updateSubscriptionFilters(subscription, filters) |
{ |
- removeSubscriptionFilters(subscription); |
+ disconnectSubscriptionFilters(subscription); |
let oldFilters = subscription.filters; |
subscription.filters = filters; |
- addSubscriptionFilters(subscription); |
+ connectSubscriptionFilters(subscription); |
filterNotifier.emit("subscription.updated", subscription, oldFilters); |
- }, |
+ } |
/** |
- * Adds a user-defined filter to the list |
+ * Adds a user-defined filter to the storage. |
* @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 |
+ * @param {?SpecialSubscription} [subscription] The subscription that the |
+ * filter should be added to. |
+ * @param {number} [position] The position within the subscription at which |
+ * the filter should be added. If not specified, the filter is added at the |
+ * end of the subscription. |
*/ |
addFilter(filter, subscription, position) |
{ |
if (!subscription) |
{ |
for (let currentSubscription of filter.subscriptions()) |
{ |
if (currentSubscription instanceof SpecialSubscription && |
!currentSubscription.disabled) |
{ |
return; // No need to add |
} |
} |
- subscription = FilterStorage.getGroupForFilter(filter); |
+ subscription = this.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; |
filter.addSubscription(subscription); |
subscription.filters.splice(position, 0, filter); |
filterNotifier.emit("filter.added", filter, subscription, position); |
- }, |
+ } |
/** |
- * Removes a user-defined filter from the list |
+ * Removes a user-defined filter from the storage. |
* @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) |
+ * @param {?SpecialSubscription} [subscription] The subscription that the |
+ * filter should be removed from. If not specified, the filter will be |
+ * removed from all subscriptions. |
+ * @param {number} [position] The position within the subscription at which |
+ * the filter should be removed. If not specified, all instances of the |
+ * filter will be removed. |
*/ |
removeFilter(filter, subscription, position) |
{ |
let subscriptions = ( |
subscription ? [subscription] : filter.subscriptions() |
); |
for (let currentSubscription of subscriptions) |
{ |
@@ -259,25 +285,25 @@ |
if (currentSubscription.filters.indexOf(filter) < 0) |
filter.removeSubscription(currentSubscription); |
filterNotifier.emit("filter.removed", filter, currentSubscription, |
currentPosition); |
} |
} |
} |
} |
- }, |
+ } |
/** |
- * Moves a user-defined filter to a new position |
+ * 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 |
+ * @param {SpecialSubscription} subscription The subscription where the |
+ * filter is located. |
+ * @param {number} oldPosition The current position of the filter. |
+ * @param {number} newPosition The new position of the filter. |
*/ |
moveFilter(filter, subscription, oldPosition, newPosition) |
{ |
if (!(subscription instanceof SpecialSubscription) || |
subscription.filters[oldPosition] != filter) |
{ |
return; |
} |
@@ -286,61 +312,60 @@ |
subscription.filters.length - 1); |
if (oldPosition == newPosition) |
return; |
subscription.filters.splice(oldPosition, 1); |
subscription.filters.splice(newPosition, 0, filter); |
filterNotifier.emit("filter.moved", filter, subscription, oldPosition, |
newPosition); |
- }, |
+ } |
/** |
- * Increases the hit count for a filter by one |
+ * 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 |
+ * Resets hit count for some filters. |
+ * @param {?Array.<Filter>} [filters] The filters to be reset. If not |
+ * specified, all filters will be reset. |
*/ |
resetHitCounts(filters) |
{ |
if (!filters) |
filters = Filter.knownFilters.values(); |
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. |
+ * @param {boolean} silent If <code>true</code>, no "load" notification will |
+ * be sent out. |
+ * @returns {TextSink} The function to be called for each line of data. |
+ * Calling it with <code>null</code> as the argument 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) |
@@ -353,21 +378,21 @@ |
this.knownSubscriptions = knownSubscriptions; |
Filter.knownFilters = parser.knownFilters; |
Subscription.knownSubscriptions = parser.knownSubscriptions; |
if (!silent) |
filterNotifier.emit("load"); |
} |
}; |
- }, |
+ } |
/** |
- * Loads all subscriptions from the disk. |
- * @return {Promise} promise resolved or rejected when loading is complete |
+ * Loads all subscriptions from disk. |
+ * @returns {Promise} A promise resolved or rejected when loading is complete. |
*/ |
loadFromDisk() |
{ |
let tryBackup = backupIndex => |
{ |
return this.restoreBackup(backupIndex, true).then(() => |
{ |
if (this.knownSubscriptions.size == 0) |
@@ -400,68 +425,70 @@ |
{ |
Cu.reportError(error); |
return tryBackup(1); |
}).then(() => |
{ |
this.initialized = true; |
filterNotifier.emit("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 |
+ * Constructs the file name for a <code>patterns.ini</code> backup. |
+ * @param {number} backupIndex Number of the backup file (1 being the most |
+ * recent). |
+ * @returns {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 |
+ * @param {number} backupIndex Number of the backup to restore (1 being the |
+ * most recent). |
+ * @param {boolean} silent If <code>true</code>, no "load" notification will |
+ * be sent out. |
+ * @returns {Promise} A promise resolved or rejected when restoration 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. |
+ * @yields {string} |
*/ |
*exportData() |
{ |
// Do not persist external subscriptions |
let subscriptions = []; |
for (let subscription of this.subscriptions()) |
{ |
if (!(subscription instanceof ExternalSubscription) && |
!(subscription instanceof SpecialSubscription && |
subscription.filters.length == 0)) |
{ |
subscriptions.push(subscription); |
} |
} |
yield "# Adblock Plus preferences"; |
- yield "version=" + formatVersion; |
+ yield "version=" + this.formatVersion; |
let saved = new Set(); |
let buf = []; |
// Save subscriptions |
for (let subscription of subscriptions) |
{ |
yield ""; |
@@ -487,34 +514,21 @@ |
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 |
+ * Saves all subscriptions back to disk. |
+ * @returns {Promise} A promise resolved or rejected when saving is complete. |
*/ |
saveToDisk() |
{ |
if (this._saving) |
{ |
this._needsSave = true; |
return; |
} |
@@ -588,28 +602,28 @@ |
{ |
this._saving = false; |
if (this._needsSave) |
{ |
this._needsSave = false; |
this.saveToDisk(); |
} |
}); |
- }, |
+ } |
/** |
* @typedef FileInfo |
* @type {object} |
* @property {number} index |
* @property {number} lastModified |
*/ |
/** |
* Returns a promise resolving in a list of existing backup files. |
- * @return {Promise.<FileInfo[]>} |
+ * @returns {Promise.<Array.<FileInfo>>} |
*/ |
getBackupFiles() |
{ |
let backups = []; |
let checkBackupFile = index => |
{ |
return IO.statFile(this.getBackupName(index)).then(statData => |
@@ -627,37 +641,45 @@ |
// 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 |
+ * Reads the user's filters from disk, manages them in memory, and writes them |
+ * back to disk. |
*/ |
-function addSubscriptionFilters(subscription) |
+let filterStorage = new FilterStorage(); |
+ |
+exports.filterStorage = filterStorage; |
+ |
+/** |
+ * Connects a subscription to its filters without any notifications. |
+ * @param {Subscription} subscription The subscription that should be |
+ * connected to its filters. |
+ */ |
+function connectSubscriptionFilters(subscription) |
{ |
- if (!FilterStorage.knownSubscriptions.has(subscription.url)) |
+ if (!filterStorage.knownSubscriptions.has(subscription.url)) |
return; |
for (let filter of subscription.filters) |
filter.addSubscription(subscription); |
} |
/** |
- * Removes subscription's filters from the subscription without any |
- * notifications. |
- * @param {Subscription} subscription filter subscription to be removed |
+ * Disconnects a subscription from its filters without any notifications. |
+ * @param {Subscription} subscription The subscription that should be |
+ * disconnected from its filters. |
*/ |
-function removeSubscriptionFilters(subscription) |
+function disconnectSubscriptionFilters(subscription) |
{ |
- if (!FilterStorage.knownSubscriptions.has(subscription.url)) |
+ if (!filterStorage.knownSubscriptions.has(subscription.url)) |
return; |
for (let filter of subscription.filters) |
filter.removeSubscription(subscription); |
} |