| 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); | 
| } | 
|  |