| OLD | NEW | 
|---|
| 1 /* | 1 /* | 
| 2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 
| 3  * Copyright (C) 2006-present eyeo GmbH | 3  * Copyright (C) 2006-present eyeo GmbH | 
| 4  * | 4  * | 
| 5  * Adblock Plus is free software: you can redistribute it and/or modify | 5  * Adblock Plus is free software: you can redistribute it and/or modify | 
| 6  * it under the terms of the GNU General Public License version 3 as | 6  * it under the terms of the GNU General Public License version 3 as | 
| 7  * published by the Free Software Foundation. | 7  * published by the Free Software Foundation. | 
| 8  * | 8  * | 
| 9  * Adblock Plus is distributed in the hope that it will be useful, | 9  * Adblock Plus is distributed in the hope that it will be useful, | 
| 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
| 11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
| 12  * GNU General Public License for more details. | 12  * GNU General Public License for more details. | 
| 13  * | 13  * | 
| 14  * You should have received a copy of the GNU General Public License | 14  * You should have received a copy of the GNU General Public License | 
| 15  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 15  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
| 16  */ | 16  */ | 
| 17 | 17 | 
| 18 "use strict"; | 18 "use strict"; | 
| 19 | 19 | 
| 20 /** | 20 const {FilterStorage} = require("compiled"); | 
| 21  * @fileOverview FilterStorage class responsible for managing user's | 21 const {Subscription, SpecialSubscription} = require("subscriptionClasses"); | 
| 22  *               subscriptions and filters. |  | 
| 23  */ |  | 
| 24 | 22 | 
| 25 const {IO} = require("io"); | 23 // Backwards compatibility | 
| 26 const {Prefs} = require("prefs"); | 24 FilterStorage.getGroupForFilter = FilterStorage.getSubscriptionForFilter; | 
| 27 const {Filter, ActiveFilter} = require("filterClasses"); |  | 
| 28 const {Subscription, SpecialSubscription, |  | 
| 29        ExternalSubscription} = require("subscriptionClasses"); |  | 
| 30 const {FilterNotifier} = require("filterNotifier"); |  | 
| 31 | 25 | 
| 32 /** | 26 /** | 
| 33  * Version number of the filter storage file format. | 27  * This property allows iterating over the list of subscriptions. It will delete | 
| 34  * @type {number} | 28  * references automatically at the end of the current loop iteration. If you | 
|  | 29  * need persistent references or element access by position you should use | 
|  | 30  * FilterStorage.subscriptionAt() instead. | 
|  | 31  * @type {Iterable} | 
| 35  */ | 32  */ | 
| 36 let formatVersion = 5; | 33 FilterStorage.subscriptions = { | 
| 37 | 34   *[Symbol.iterator]() | 
| 38 /** |  | 
| 39  * This class reads user's filters from disk, manages them in memory |  | 
| 40  * and writes them back. |  | 
| 41  * @class |  | 
| 42  */ |  | 
| 43 let FilterStorage = exports.FilterStorage = |  | 
| 44 { |  | 
| 45   /** |  | 
| 46    * Will be set to true after the initial loadFromDisk() call completes. |  | 
| 47    * @type {boolean} |  | 
| 48    */ |  | 
| 49   initialized: false, |  | 
| 50 |  | 
| 51   /** |  | 
| 52    * Version number of the patterns.ini format used. |  | 
| 53    * @type {number} |  | 
| 54    */ |  | 
| 55   get formatVersion() |  | 
| 56   { | 35   { | 
| 57     return formatVersion; | 36     for (let i = 0, l = FilterStorage.subscriptionCount; i < l; i++) | 
| 58   }, |  | 
| 59 |  | 
| 60   /** |  | 
| 61    * File containing the filter list |  | 
| 62    * @type {string} |  | 
| 63    */ |  | 
| 64   get sourceFile() |  | 
| 65   { |  | 
| 66     return "patterns.ini"; |  | 
| 67   }, |  | 
| 68 |  | 
| 69   /** |  | 
| 70    * Will be set to true if no patterns.ini file exists. |  | 
| 71    * @type {boolean} |  | 
| 72    */ |  | 
| 73   firstRun: false, |  | 
| 74 |  | 
| 75   /** |  | 
| 76    * Map of properties listed in the filter storage file before the sections |  | 
| 77    * start. Right now this should be only the format version. |  | 
| 78    */ |  | 
| 79   fileProperties: Object.create(null), |  | 
| 80 |  | 
| 81   /** |  | 
| 82    * List of filter subscriptions containing all filters |  | 
| 83    * @type {Subscription[]} |  | 
| 84    */ |  | 
| 85   subscriptions: [], |  | 
| 86 |  | 
| 87   /** |  | 
| 88    * Map of subscriptions already on the list, by their URL/identifier |  | 
| 89    * @type {Object} |  | 
| 90    */ |  | 
| 91   knownSubscriptions: Object.create(null), |  | 
| 92 |  | 
| 93   /** |  | 
| 94    * Finds the filter group that a filter should be added to by default. Will |  | 
| 95    * return null if this group doesn't exist yet. |  | 
| 96    * @param {Filter} filter |  | 
| 97    * @return {?SpecialSubscription} |  | 
| 98    */ |  | 
| 99   getGroupForFilter(filter) |  | 
| 100   { |  | 
| 101     let generalSubscription = null; |  | 
| 102     for (let subscription of FilterStorage.subscriptions) |  | 
| 103     { | 37     { | 
| 104       if (subscription instanceof SpecialSubscription && !subscription.disabled) | 38       let subscription = FilterStorage.subscriptionAt(i); | 
|  | 39       try | 
| 105       { | 40       { | 
| 106         // Always prefer specialized subscriptions | 41         yield subscription; | 
| 107         if (subscription.isDefaultFor(filter)) | 42       } | 
| 108           return subscription; | 43       finally | 
| 109 | 44       { | 
| 110         // If this is a general subscription - store it as fallback | 45         subscription.delete(); | 
| 111         if (!generalSubscription && |  | 
| 112             (!subscription.defaults || !subscription.defaults.length)) |  | 
| 113         { |  | 
| 114           generalSubscription = subscription; |  | 
| 115         } |  | 
| 116       } | 46       } | 
| 117     } | 47     } | 
| 118     return generalSubscription; |  | 
| 119   }, |  | 
| 120 |  | 
| 121   /** |  | 
| 122    * Adds a filter subscription to the list |  | 
| 123    * @param {Subscription} subscription filter subscription to be added |  | 
| 124    */ |  | 
| 125   addSubscription(subscription) |  | 
| 126   { |  | 
| 127     if (subscription.url in FilterStorage.knownSubscriptions) |  | 
| 128       return; |  | 
| 129 |  | 
| 130     FilterStorage.subscriptions.push(subscription); |  | 
| 131     FilterStorage.knownSubscriptions[subscription.url] = subscription; |  | 
| 132     addSubscriptionFilters(subscription); |  | 
| 133 |  | 
| 134     FilterNotifier.triggerListeners("subscription.added", subscription); |  | 
| 135   }, |  | 
| 136 |  | 
| 137   /** |  | 
| 138    * Removes a filter subscription from the list |  | 
| 139    * @param {Subscription} subscription filter subscription to be removed |  | 
| 140    */ |  | 
| 141   removeSubscription(subscription) |  | 
| 142   { |  | 
| 143     for (let i = 0; i < FilterStorage.subscriptions.length; i++) |  | 
| 144     { |  | 
| 145       if (FilterStorage.subscriptions[i].url == subscription.url) |  | 
| 146       { |  | 
| 147         removeSubscriptionFilters(subscription); |  | 
| 148 |  | 
| 149         FilterStorage.subscriptions.splice(i--, 1); |  | 
| 150         delete FilterStorage.knownSubscriptions[subscription.url]; |  | 
| 151         FilterNotifier.triggerListeners("subscription.removed", subscription); |  | 
| 152         return; |  | 
| 153       } |  | 
| 154     } |  | 
| 155   }, |  | 
| 156 |  | 
| 157   /** |  | 
| 158    * Moves a subscription in the list to a new position. |  | 
| 159    * @param {Subscription} subscription filter subscription to be moved |  | 
| 160    * @param {Subscription} [insertBefore] filter subscription to insert before |  | 
| 161    *        (if omitted the subscription will be put at the end of the list) |  | 
| 162    */ |  | 
| 163   moveSubscription(subscription, insertBefore) |  | 
| 164   { |  | 
| 165     let currentPos = FilterStorage.subscriptions.indexOf(subscription); |  | 
| 166     if (currentPos < 0) |  | 
| 167       return; |  | 
| 168 |  | 
| 169     let newPos = -1; |  | 
| 170     if (insertBefore) |  | 
| 171       newPos = FilterStorage.subscriptions.indexOf(insertBefore); |  | 
| 172 |  | 
| 173     if (newPos < 0) |  | 
| 174       newPos = FilterStorage.subscriptions.length; |  | 
| 175 |  | 
| 176     if (currentPos < newPos) |  | 
| 177       newPos--; |  | 
| 178     if (currentPos == newPos) |  | 
| 179       return; |  | 
| 180 |  | 
| 181     FilterStorage.subscriptions.splice(currentPos, 1); |  | 
| 182     FilterStorage.subscriptions.splice(newPos, 0, subscription); |  | 
| 183     FilterNotifier.triggerListeners("subscription.moved", subscription); |  | 
| 184   }, |  | 
| 185 |  | 
| 186   /** |  | 
| 187    * Replaces the list of filters in a subscription by a new list |  | 
| 188    * @param {Subscription} subscription filter subscription to be updated |  | 
| 189    * @param {Filter[]} filters new filter list |  | 
| 190    */ |  | 
| 191   updateSubscriptionFilters(subscription, filters) |  | 
| 192   { |  | 
| 193     removeSubscriptionFilters(subscription); |  | 
| 194     subscription.oldFilters = subscription.filters; |  | 
| 195     subscription.filters = filters; |  | 
| 196     addSubscriptionFilters(subscription); |  | 
| 197     FilterNotifier.triggerListeners("subscription.updated", subscription); |  | 
| 198     delete subscription.oldFilters; |  | 
| 199   }, |  | 
| 200 |  | 
| 201   /** |  | 
| 202    * Adds a user-defined filter to the list |  | 
| 203    * @param {Filter} filter |  | 
| 204    * @param {SpecialSubscription} [subscription] |  | 
| 205    *   particular group that the filter should be added to |  | 
| 206    * @param {number} [position] |  | 
| 207    *   position within the subscription at which the filter should be added |  | 
| 208    */ |  | 
| 209   addFilter(filter, subscription, position) |  | 
| 210   { |  | 
| 211     if (!subscription) |  | 
| 212     { |  | 
| 213       if (filter.subscriptions.some(s => s instanceof SpecialSubscription && |  | 
| 214                                          !s.disabled)) |  | 
| 215       { |  | 
| 216         return;   // No need to add |  | 
| 217       } |  | 
| 218       subscription = FilterStorage.getGroupForFilter(filter); |  | 
| 219     } |  | 
| 220     if (!subscription) |  | 
| 221     { |  | 
| 222       // No group for this filter exists, create one |  | 
| 223       subscription = SpecialSubscription.createForFilter(filter); |  | 
| 224       this.addSubscription(subscription); |  | 
| 225       return; |  | 
| 226     } |  | 
| 227 |  | 
| 228     if (typeof position == "undefined") |  | 
| 229       position = subscription.filters.length; |  | 
| 230 |  | 
| 231     if (filter.subscriptions.indexOf(subscription) < 0) |  | 
| 232       filter.subscriptions.push(subscription); |  | 
| 233     subscription.filters.splice(position, 0, filter); |  | 
| 234     FilterNotifier.triggerListeners("filter.added", filter, subscription, |  | 
| 235                                     position); |  | 
| 236   }, |  | 
| 237 |  | 
| 238   /** |  | 
| 239    * Removes a user-defined filter from the list |  | 
| 240    * @param {Filter} filter |  | 
| 241    * @param {SpecialSubscription} [subscription] a particular filter group that |  | 
| 242    *      the filter should be removed from (if ommited will be removed from all |  | 
| 243    *      subscriptions) |  | 
| 244    * @param {number} [position]  position inside the filter group at which the |  | 
| 245    *      filter should be removed (if ommited all instances will be removed) |  | 
| 246    */ |  | 
| 247   removeFilter(filter, subscription, position) |  | 
| 248   { |  | 
| 249     let subscriptions = ( |  | 
| 250       subscription ? [subscription] : filter.subscriptions.slice() |  | 
| 251     ); |  | 
| 252     for (let i = 0; i < subscriptions.length; i++) |  | 
| 253     { |  | 
| 254       let currentSubscription = subscriptions[i]; |  | 
| 255       if (currentSubscription instanceof SpecialSubscription) |  | 
| 256       { |  | 
| 257         let positions = []; |  | 
| 258         if (typeof position == "undefined") |  | 
| 259         { |  | 
| 260           let index = -1; |  | 
| 261           do |  | 
| 262           { |  | 
| 263             index = currentSubscription.filters.indexOf(filter, index + 1); |  | 
| 264             if (index >= 0) |  | 
| 265               positions.push(index); |  | 
| 266           } while (index >= 0); |  | 
| 267         } |  | 
| 268         else |  | 
| 269           positions.push(position); |  | 
| 270 |  | 
| 271         for (let j = positions.length - 1; j >= 0; j--) |  | 
| 272         { |  | 
| 273           let currentPosition = positions[j]; |  | 
| 274           if (currentSubscription.filters[currentPosition] == filter) |  | 
| 275           { |  | 
| 276             currentSubscription.filters.splice(currentPosition, 1); |  | 
| 277             if (currentSubscription.filters.indexOf(filter) < 0) |  | 
| 278             { |  | 
| 279               let index = filter.subscriptions.indexOf(currentSubscription); |  | 
| 280               if (index >= 0) |  | 
| 281                 filter.subscriptions.splice(index, 1); |  | 
| 282             } |  | 
| 283             FilterNotifier.triggerListeners( |  | 
| 284               "filter.removed", filter, currentSubscription, currentPosition |  | 
| 285             ); |  | 
| 286           } |  | 
| 287         } |  | 
| 288       } |  | 
| 289     } |  | 
| 290   }, |  | 
| 291 |  | 
| 292   /** |  | 
| 293    * Moves a user-defined filter to a new position |  | 
| 294    * @param {Filter} filter |  | 
| 295    * @param {SpecialSubscription} subscription filter group where the filter is |  | 
| 296    *                                           located |  | 
| 297    * @param {number} oldPosition current position of the filter |  | 
| 298    * @param {number} newPosition new position of the filter |  | 
| 299    */ |  | 
| 300   moveFilter(filter, subscription, oldPosition, newPosition) |  | 
| 301   { |  | 
| 302     if (!(subscription instanceof SpecialSubscription) || |  | 
| 303         subscription.filters[oldPosition] != filter) |  | 
| 304     { |  | 
| 305       return; |  | 
| 306     } |  | 
| 307 |  | 
| 308     newPosition = Math.min(Math.max(newPosition, 0), |  | 
| 309                            subscription.filters.length - 1); |  | 
| 310     if (oldPosition == newPosition) |  | 
| 311       return; |  | 
| 312 |  | 
| 313     subscription.filters.splice(oldPosition, 1); |  | 
| 314     subscription.filters.splice(newPosition, 0, filter); |  | 
| 315     FilterNotifier.triggerListeners("filter.moved", filter, subscription, |  | 
| 316                                     oldPosition, newPosition); |  | 
| 317   }, |  | 
| 318 |  | 
| 319   /** |  | 
| 320    * Increases the hit count for a filter by one |  | 
| 321    * @param {Filter} filter |  | 
| 322    */ |  | 
| 323   increaseHitCount(filter) |  | 
| 324   { |  | 
| 325     if (!Prefs.savestats || !(filter instanceof ActiveFilter)) |  | 
| 326       return; |  | 
| 327 |  | 
| 328     filter.hitCount++; |  | 
| 329     filter.lastHit = Date.now(); |  | 
| 330   }, |  | 
| 331 |  | 
| 332   /** |  | 
| 333    * Resets hit count for some filters |  | 
| 334    * @param {Filter[]} filters  filters to be reset, if null all filters will |  | 
| 335    *                            be reset |  | 
| 336    */ |  | 
| 337   resetHitCounts(filters) |  | 
| 338   { |  | 
| 339     if (!filters) |  | 
| 340     { |  | 
| 341       filters = []; |  | 
| 342       for (let text in Filter.knownFilters) |  | 
| 343         filters.push(Filter.knownFilters[text]); |  | 
| 344     } |  | 
| 345     for (let filter of filters) |  | 
| 346     { |  | 
| 347       filter.hitCount = 0; |  | 
| 348       filter.lastHit = 0; |  | 
| 349     } |  | 
| 350   }, |  | 
| 351 |  | 
| 352   /** |  | 
| 353    * @callback TextSink |  | 
| 354    * @param {string?} line |  | 
| 355    */ |  | 
| 356 |  | 
| 357   /** |  | 
| 358    * Allows importing previously serialized filter data. |  | 
| 359    * @param {boolean} silent |  | 
| 360    *    If true, no "load" notification will be sent out. |  | 
| 361    * @return {TextSink} |  | 
| 362    *    Function to be called for each line of data. Calling it with null as |  | 
| 363    *    parameter finalizes the import and replaces existing data. No changes |  | 
| 364    *    will be applied before finalization, so import can be "aborted" by |  | 
| 365    *    forgetting this callback. |  | 
| 366    */ |  | 
| 367   importData(silent) |  | 
| 368   { |  | 
| 369     let parser = new INIParser(); |  | 
| 370     return line => |  | 
| 371     { |  | 
| 372       parser.process(line); |  | 
| 373       if (line === null) |  | 
| 374       { |  | 
| 375         let knownSubscriptions = Object.create(null); |  | 
| 376         for (let subscription of parser.subscriptions) |  | 
| 377           knownSubscriptions[subscription.url] = subscription; |  | 
| 378 |  | 
| 379         this.fileProperties = parser.fileProperties; |  | 
| 380         this.subscriptions = parser.subscriptions; |  | 
| 381         this.knownSubscriptions = knownSubscriptions; |  | 
| 382         Filter.knownFilters = parser.knownFilters; |  | 
| 383         Subscription.knownSubscriptions = parser.knownSubscriptions; |  | 
| 384 |  | 
| 385         if (!silent) |  | 
| 386           FilterNotifier.triggerListeners("load"); |  | 
| 387       } |  | 
| 388     }; |  | 
| 389   }, |  | 
| 390 |  | 
| 391   /** |  | 
| 392    * Loads all subscriptions from the disk. |  | 
| 393    * @return {Promise} promise resolved or rejected when loading is complete |  | 
| 394    */ |  | 
| 395   loadFromDisk() |  | 
| 396   { |  | 
| 397     let tryBackup = backupIndex => |  | 
| 398     { |  | 
| 399       return this.restoreBackup(backupIndex, true).then(() => |  | 
| 400       { |  | 
| 401         if (this.subscriptions.length == 0) |  | 
| 402           return tryBackup(backupIndex + 1); |  | 
| 403       }).catch(error => |  | 
| 404       { |  | 
| 405         // Give up |  | 
| 406       }); |  | 
| 407     }; |  | 
| 408 |  | 
| 409     return IO.statFile(this.sourceFile).then(statData => |  | 
| 410     { |  | 
| 411       if (!statData.exists) |  | 
| 412       { |  | 
| 413         this.firstRun = true; |  | 
| 414         return; |  | 
| 415       } |  | 
| 416 |  | 
| 417       let parser = this.importData(true); |  | 
| 418       return IO.readFromFile(this.sourceFile, parser).then(() => |  | 
| 419       { |  | 
| 420         parser(null); |  | 
| 421         if (this.subscriptions.length == 0) |  | 
| 422         { |  | 
| 423           // No filter subscriptions in the file, this isn't right. |  | 
| 424           throw new Error("No data in the file"); |  | 
| 425         } |  | 
| 426       }); |  | 
| 427     }).catch(error => |  | 
| 428     { |  | 
| 429       Cu.reportError(error); |  | 
| 430       return tryBackup(1); |  | 
| 431     }).then(() => |  | 
| 432     { |  | 
| 433       this.initialized = true; |  | 
| 434       FilterNotifier.triggerListeners("load"); |  | 
| 435     }); |  | 
| 436   }, |  | 
| 437 |  | 
| 438   /** |  | 
| 439    * Constructs the file name for a patterns.ini backup. |  | 
| 440    * @param {number} backupIndex |  | 
| 441    *    number of the backup file (1 being the most recent) |  | 
| 442    * @return {string} backup file name |  | 
| 443    */ |  | 
| 444   getBackupName(backupIndex) |  | 
| 445   { |  | 
| 446     let [name, extension] = this.sourceFile.split(".", 2); |  | 
| 447     return (name + "-backup" + backupIndex + "." + extension); |  | 
| 448   }, |  | 
| 449 |  | 
| 450   /** |  | 
| 451    * Restores an automatically created backup. |  | 
| 452    * @param {number} backupIndex |  | 
| 453    *    number of the backup to restore (1 being the most recent) |  | 
| 454    * @param {boolean} silent |  | 
| 455    *    If true, no "load" notification will be sent out. |  | 
| 456    * @return {Promise} promise resolved or rejected when restoring is complete |  | 
| 457    */ |  | 
| 458   restoreBackup(backupIndex, silent) |  | 
| 459   { |  | 
| 460     let backupFile = this.getBackupName(backupIndex); |  | 
| 461     let parser = this.importData(silent); |  | 
| 462     return IO.readFromFile(backupFile, parser).then(() => |  | 
| 463     { |  | 
| 464       parser(null); |  | 
| 465       return this.saveToDisk(); |  | 
| 466     }); |  | 
| 467   }, |  | 
| 468 |  | 
| 469   /** |  | 
| 470    * Generator serializing filter data and yielding it line by line. |  | 
| 471    */ |  | 
| 472   *exportData() |  | 
| 473   { |  | 
| 474     // Do not persist external subscriptions |  | 
| 475     let subscriptions = this.subscriptions.filter( |  | 
| 476       s => !(s instanceof ExternalSubscription) |  | 
| 477     ); |  | 
| 478 |  | 
| 479     yield "# Adblock Plus preferences"; |  | 
| 480     yield "version=" + formatVersion; |  | 
| 481 |  | 
| 482     let saved = new Set(); |  | 
| 483     let buf = []; |  | 
| 484 |  | 
| 485     // Save subscriptions |  | 
| 486     for (let subscription of subscriptions) |  | 
| 487     { |  | 
| 488       yield ""; |  | 
| 489 |  | 
| 490       subscription.serialize(buf); |  | 
| 491       if (subscription.filters.length) |  | 
| 492       { |  | 
| 493         buf.push("", "[Subscription filters]"); |  | 
| 494         subscription.serializeFilters(buf); |  | 
| 495       } |  | 
| 496       for (let line of buf) |  | 
| 497         yield line; |  | 
| 498       buf.splice(0); |  | 
| 499     } |  | 
| 500 |  | 
| 501     // Save filter data |  | 
| 502     for (let subscription of subscriptions) |  | 
| 503     { |  | 
| 504       for (let filter of subscription.filters) |  | 
| 505       { |  | 
| 506         if (!saved.has(filter.text)) |  | 
| 507         { |  | 
| 508           filter.serialize(buf); |  | 
| 509           saved.add(filter.text); |  | 
| 510           for (let line of buf) |  | 
| 511             yield line; |  | 
| 512           buf.splice(0); |  | 
| 513         } |  | 
| 514       } |  | 
| 515     } |  | 
| 516   }, |  | 
| 517 |  | 
| 518   /** |  | 
| 519    * Will be set to true if saveToDisk() is running (reentrance protection). |  | 
| 520    * @type {boolean} |  | 
| 521    */ |  | 
| 522   _saving: false, |  | 
| 523 |  | 
| 524   /** |  | 
| 525    * Will be set to true if a saveToDisk() call arrives while saveToDisk() is |  | 
| 526    * already running (delayed execution). |  | 
| 527    * @type {boolean} |  | 
| 528    */ |  | 
| 529   _needsSave: false, |  | 
| 530 |  | 
| 531   /** |  | 
| 532    * Saves all subscriptions back to disk |  | 
| 533    * @return {Promise} promise resolved or rejected when saving is complete |  | 
| 534    */ |  | 
| 535   saveToDisk() |  | 
| 536   { |  | 
| 537     if (this._saving) |  | 
| 538     { |  | 
| 539       this._needsSave = true; |  | 
| 540       return; |  | 
| 541     } |  | 
| 542 |  | 
| 543     this._saving = true; |  | 
| 544 |  | 
| 545     return Promise.resolve().then(() => |  | 
| 546     { |  | 
| 547       // First check whether we need to create a backup |  | 
| 548       if (Prefs.patternsbackups <= 0) |  | 
| 549         return false; |  | 
| 550 |  | 
| 551       return IO.statFile(this.sourceFile).then(statData => |  | 
| 552       { |  | 
| 553         if (!statData.exists) |  | 
| 554           return false; |  | 
| 555 |  | 
| 556         return IO.statFile(this.getBackupName(1)).then(backupStatData => |  | 
| 557         { |  | 
| 558           if (backupStatData.exists && |  | 
| 559               (Date.now() - backupStatData.lastModified) / 3600000 < |  | 
| 560                 Prefs.patternsbackupinterval) |  | 
| 561           { |  | 
| 562             return false; |  | 
| 563           } |  | 
| 564           return true; |  | 
| 565         }); |  | 
| 566       }); |  | 
| 567     }).then(backupRequired => |  | 
| 568     { |  | 
| 569       if (!backupRequired) |  | 
| 570         return; |  | 
| 571 |  | 
| 572       let ignoreErrors = error => |  | 
| 573       { |  | 
| 574         // Expected error, backup file doesn't exist. |  | 
| 575       }; |  | 
| 576 |  | 
| 577       let renameBackup = index => |  | 
| 578       { |  | 
| 579         if (index > 0) |  | 
| 580         { |  | 
| 581           return IO.renameFile(this.getBackupName(index), |  | 
| 582                                this.getBackupName(index + 1)) |  | 
| 583                    .catch(ignoreErrors) |  | 
| 584                    .then(() => renameBackup(index - 1)); |  | 
| 585         } |  | 
| 586 |  | 
| 587         return IO.renameFile(this.sourceFile, this.getBackupName(1)) |  | 
| 588                  .catch(ignoreErrors); |  | 
| 589       }; |  | 
| 590 |  | 
| 591       // Rename existing files |  | 
| 592       return renameBackup(Prefs.patternsbackups - 1); |  | 
| 593     }).catch(error => |  | 
| 594     { |  | 
| 595       // Errors during backup creation shouldn't prevent writing filters. |  | 
| 596       Cu.reportError(error); |  | 
| 597     }).then(() => |  | 
| 598     { |  | 
| 599       return IO.writeToFile(this.sourceFile, this.exportData()); |  | 
| 600     }).then(() => |  | 
| 601     { |  | 
| 602       FilterNotifier.triggerListeners("save"); |  | 
| 603     }).catch(error => |  | 
| 604     { |  | 
| 605       // If saving failed, report error but continue - we still have to process |  | 
| 606       // flags. |  | 
| 607       Cu.reportError(error); |  | 
| 608     }).then(() => |  | 
| 609     { |  | 
| 610       this._saving = false; |  | 
| 611       if (this._needsSave) |  | 
| 612       { |  | 
| 613         this._needsSave = false; |  | 
| 614         this.saveToDisk(); |  | 
| 615       } |  | 
| 616     }); |  | 
| 617   }, |  | 
| 618 |  | 
| 619   /** |  | 
| 620    * @typedef FileInfo |  | 
| 621    * @type {object} |  | 
| 622    * @property {nsIFile} file |  | 
| 623    * @property {number} lastModified |  | 
| 624    */ |  | 
| 625 |  | 
| 626   /** |  | 
| 627    * Returns a promise resolving in a list of existing backup files. |  | 
| 628    * @return {Promise.<FileInfo[]>} |  | 
| 629    */ |  | 
| 630   getBackupFiles() |  | 
| 631   { |  | 
| 632     let backups = []; |  | 
| 633 |  | 
| 634     let checkBackupFile = index => |  | 
| 635     { |  | 
| 636       return IO.statFile(this.getBackupName(index)).then(statData => |  | 
| 637       { |  | 
| 638         if (!statData.exists) |  | 
| 639           return backups; |  | 
| 640 |  | 
| 641         backups.push({ |  | 
| 642           index, |  | 
| 643           lastModified: statData.lastModified |  | 
| 644         }); |  | 
| 645         return checkBackupFile(index + 1); |  | 
| 646       }).catch(error => |  | 
| 647       { |  | 
| 648         // Something went wrong, return whatever data we got so far. |  | 
| 649         Cu.reportError(error); |  | 
| 650         return backups; |  | 
| 651       }); |  | 
| 652     }; |  | 
| 653 |  | 
| 654     return checkBackupFile(1); |  | 
| 655   } | 48   } | 
| 656 }; | 49 }; | 
| 657 | 50 | 
| 658 /** | 51 /** | 
| 659  * Joins subscription's filters to the subscription without any notifications. | 52  * Adds a user-defined filter to the most suitable subscription in the list, | 
| 660  * @param {Subscription} subscription | 53  * creates one if none found. | 
| 661  *   filter subscription that should be connected to its filters | 54  * @param {Filter} filter | 
|  | 55  * @returns {boolean} | 
|  | 56  *    false if the filter was already in the list and no adding was performed | 
| 662  */ | 57  */ | 
| 663 function addSubscriptionFilters(subscription) | 58 FilterStorage.addFilter = function(filter) | 
| 664 { | 59 { | 
| 665   if (!(subscription.url in FilterStorage.knownSubscriptions)) | 60   for (let subscription of this.subscriptions) | 
| 666     return; | 61     if (!subscription.disabled && subscription.indexOfFilter(filter) >= 0) | 
|  | 62       return false; | 
| 667 | 63 | 
| 668   for (let filter of subscription.filters) | 64   let subscription = this.getSubscriptionForFilter(filter); | 
| 669     filter.subscriptions.push(subscription); | 65   try | 
| 670 } | 66   { | 
|  | 67     if (!subscription) | 
|  | 68     { | 
|  | 69       subscription = Subscription.fromURL(null); | 
|  | 70       subscription.makeDefaultFor(filter); | 
|  | 71       this.addSubscription(subscription); | 
|  | 72     } | 
|  | 73     subscription.insertFilterAt(filter, subscription.filterCount); | 
|  | 74   } | 
|  | 75   finally | 
|  | 76   { | 
|  | 77     if (subscription) | 
|  | 78       subscription.delete(); | 
|  | 79   } | 
|  | 80   return true; | 
|  | 81 }; | 
| 671 | 82 | 
| 672 /** | 83 /** | 
| 673  * Removes subscription's filters from the subscription without any | 84  * Removes a user-defined filter from the list | 
| 674  * notifications. | 85  * @param {Filter} filter | 
| 675  * @param {Subscription} subscription filter subscription to be removed |  | 
| 676  */ | 86  */ | 
| 677 function removeSubscriptionFilters(subscription) | 87 FilterStorage.removeFilter = function(filter) | 
| 678 { | 88 { | 
| 679   if (!(subscription.url in FilterStorage.knownSubscriptions)) | 89   for (let subscription of this.subscriptions) | 
| 680     return; |  | 
| 681 |  | 
| 682   for (let filter of subscription.filters) |  | 
| 683   { | 90   { | 
| 684     let i = filter.subscriptions.indexOf(subscription); | 91     if (subscription instanceof SpecialSubscription) | 
| 685     if (i >= 0) |  | 
| 686       filter.subscriptions.splice(i, 1); |  | 
| 687   } |  | 
| 688 } |  | 
| 689 |  | 
| 690 /** |  | 
| 691  * Listener returned by FilterStorage.importData(), parses filter data. |  | 
| 692  * @constructor |  | 
| 693  */ |  | 
| 694 function INIParser() |  | 
| 695 { |  | 
| 696   this.fileProperties = this.curObj = {}; |  | 
| 697   this.subscriptions = []; |  | 
| 698   this.knownFilters = Object.create(null); |  | 
| 699   this.knownSubscriptions = Object.create(null); |  | 
| 700 } |  | 
| 701 INIParser.prototype = |  | 
| 702 { |  | 
| 703   linesProcessed: 0, |  | 
| 704   subscriptions: null, |  | 
| 705   knownFilters: null, |  | 
| 706   knownSubscriptions: null, |  | 
| 707   wantObj: true, |  | 
| 708   fileProperties: null, |  | 
| 709   curObj: null, |  | 
| 710   curSection: null, |  | 
| 711 |  | 
| 712   process(val) |  | 
| 713   { |  | 
| 714     let origKnownFilters = Filter.knownFilters; |  | 
| 715     Filter.knownFilters = this.knownFilters; |  | 
| 716     let origKnownSubscriptions = Subscription.knownSubscriptions; |  | 
| 717     Subscription.knownSubscriptions = this.knownSubscriptions; |  | 
| 718     let match; |  | 
| 719     try |  | 
| 720     { | 92     { | 
| 721       if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val))) | 93       while (true) | 
| 722         this.curObj[match[1]] = match[2]; |  | 
| 723       else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val))) |  | 
| 724       { | 94       { | 
| 725         if (this.curObj) | 95         let index = subscription.indexOfFilter(filter); | 
| 726         { | 96         if (index >= 0) | 
| 727           // Process current object before going to next section | 97           subscription.removeFilterAt(index); | 
| 728           switch (this.curSection) | 98         else | 
| 729           { | 99           break; | 
| 730             case "filter": |  | 
| 731               if ("text" in this.curObj) |  | 
| 732                 Filter.fromObject(this.curObj); |  | 
| 733               break; |  | 
| 734             case "subscription": { |  | 
| 735               let subscription = Subscription.fromObject(this.curObj); |  | 
| 736               if (subscription) |  | 
| 737                 this.subscriptions.push(subscription); |  | 
| 738               break; |  | 
| 739             } |  | 
| 740             case "subscription filters": |  | 
| 741               if (this.subscriptions.length) |  | 
| 742               { |  | 
| 743                 let subscription = this.subscriptions[ |  | 
| 744                   this.subscriptions.length - 1 |  | 
| 745                 ]; |  | 
| 746                 for (let text of this.curObj) |  | 
| 747                 { |  | 
| 748                   let filter = Filter.fromText(text); |  | 
| 749                   subscription.filters.push(filter); |  | 
| 750                   filter.subscriptions.push(subscription); |  | 
| 751                 } |  | 
| 752               } |  | 
| 753               break; |  | 
| 754           } |  | 
| 755         } |  | 
| 756 |  | 
| 757         if (val === null) |  | 
| 758           return; |  | 
| 759 |  | 
| 760         this.curSection = match[1].toLowerCase(); |  | 
| 761         switch (this.curSection) |  | 
| 762         { |  | 
| 763           case "filter": |  | 
| 764           case "subscription": |  | 
| 765             this.wantObj = true; |  | 
| 766             this.curObj = {}; |  | 
| 767             break; |  | 
| 768           case "subscription filters": |  | 
| 769             this.wantObj = false; |  | 
| 770             this.curObj = []; |  | 
| 771             break; |  | 
| 772           default: |  | 
| 773             this.wantObj = undefined; |  | 
| 774             this.curObj = null; |  | 
| 775         } |  | 
| 776       } | 100       } | 
| 777       else if (this.wantObj === false && val) |  | 
| 778         this.curObj.push(val.replace(/\\\[/g, "[")); |  | 
| 779     } |  | 
| 780     finally |  | 
| 781     { |  | 
| 782       Filter.knownFilters = origKnownFilters; |  | 
| 783       Subscription.knownSubscriptions = origKnownSubscriptions; |  | 
| 784     } | 101     } | 
| 785   } | 102   } | 
| 786 }; | 103 }; | 
|  | 104 | 
|  | 105 exports.FilterStorage = FilterStorage; | 
| OLD | NEW | 
|---|