/* * This Source Code is subject to the terms of the Mozilla Public License * version 2.0 (the "License"). You can obtain a copy of the License at * http://mozilla.org/MPL/2.0/. */ #filter substitution /** * @fileOverview Definition of Subscription class and its subclasses. */ var EXPORTED_SYMBOLS = ["Subscription", "SpecialSubscription", "RegularSubscription", "ExternalSubscription", "DownloadableSubscription"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; let baseURL = "resource://@ADDON_CHROME_NAME@/modules/"; Cu.import(baseURL + "Utils.jsm"); Cu.import(baseURL + "FilterClasses.jsm"); Cu.import(baseURL + "FilterNotifier.jsm"); /** * Abstract base class for filter subscriptions * * @param {String} url download location of the subscription * @param {String} [title] title of the filter subscription * @constructor */ function Subscription(url, title) { this.url = url; this.filters = []; this._title = title || Utils.getString("newGroup_title"); Subscription.knownSubscriptions[url] = this; } Subscription.prototype = { /** * Download location of the subscription * @type String */ url: null, /** * Filters contained in the filter subscription * @type Array of Filter */ filters: null, _title: null, _fixedTitle: false, _disabled: false, /** * Title of the filter subscription * @type String */ get title() this._title, set title(value) { if (value != this._title) { let oldValue = this._title; this._title = value; FilterNotifier.triggerListeners("subscription.title", this, value, oldValue); } return this._title; }, /** * Determines whether the title should be editable * @type Boolean */ get fixedTitle() this._fixedTitle, set fixedTitle(value) { if (value != this._fixedTitle) { let oldValue = this._fixedTitle; this._fixedTitle = value; FilterNotifier.triggerListeners("subscription.fixedTitle", this, value, oldValue); } return this._fixedTitle; }, /** * Defines whether the filters in the subscription should be disabled * @type Boolean */ get disabled() this._disabled, set disabled(value) { if (value != this._disabled) { let oldValue = this._disabled; this._disabled = value; FilterNotifier.triggerListeners("subscription.disabled", this, value, oldValue); } return this._disabled; }, /** * Serializes the filter to an array of strings for writing out on the disk. * @param {Array of String} buffer buffer to push the serialization results into */ serialize: function(buffer) { buffer.push("[Subscription]"); buffer.push("url=" + this.url); buffer.push("title=" + this._title); if (this._fixedTitle) buffer.push("fixedTitle=true"); if (this._disabled) buffer.push("disabled=true"); }, serializeFilters: function(buffer) { for each (let filter in this.filters) buffer.push(filter.text.replace(/\[/g, "\\[")); }, toString: function() { let buffer = []; this.serialize(buffer); return buffer.join("\n"); } }; /** * Cache for known filter subscriptions, maps URL to subscription objects. * @type Object */ Subscription.knownSubscriptions = {__proto__: null}; /** * Returns a subscription from its URL, creates a new one if necessary. * @param {String} url URL of the subscription * @return {Subscription} subscription or null if the subscription couldn't be created */ Subscription.fromURL = function(url) { if (url in Subscription.knownSubscriptions) return Subscription.knownSubscriptions[url]; try { // Test URL for validity url = Utils.ioService.newURI(url, null, null).spec; return new DownloadableSubscription(url, null); } catch (e) { return new SpecialSubscription(url); } } /** * Deserializes a subscription * * @param {Object} obj map of serialized properties and their values * @return {Subscription} subscription or null if the subscription couldn't be created */ Subscription.fromObject = function(obj) { let result; try { obj.url = Utils.ioService.newURI(obj.url, null, null).spec; // URL is valid - this is a downloadable subscription result = new DownloadableSubscription(obj.url, obj.title); if ("nextURL" in obj) result.nextURL = obj.nextURL; if ("downloadStatus" in obj) result._downloadStatus = obj.downloadStatus; if ("lastModified" in obj) result.lastModified = obj.lastModified; if ("lastSuccess" in obj) result.lastSuccess = parseInt(obj.lastSuccess) || 0; if ("lastCheck" in obj) result._lastCheck = parseInt(obj.lastCheck) || 0; if ("expires" in obj) result.expires = parseInt(obj.expires) || 0; if ("softExpiration" in obj) result.softExpiration = parseInt(obj.softExpiration) || 0; if ("errors" in obj) result._errors = parseInt(obj.errors) || 0; if ("requiredVersion" in obj) { result.requiredVersion = obj.requiredVersion; if (Utils.versionComparator.compare(result.requiredVersion, "3.5.0") > 0) result.upgradeRequired = true; } if ("alternativeLocations" in obj) result.alternativeLocations = obj.alternativeLocations; if ("homepage" in obj) result._homepage = obj.homepage; if ("lastDownload" in obj) result._lastDownload = parseInt(obj.lastDownload) || 0; } catch (e) { // Invalid URL - custom filter group if (!("title" in obj)) { // Backwards compatibility - titles and filter types were originally // determined by group identifier. if (obj.url == "~wl~") obj.defaults = "whitelist"; else if (obj.url == "~fl~") obj.defaults = "blocking"; else if (obj.url == "~eh~") obj.defaults = "elemhide"; if ("defaults" in obj) obj.title = Utils.getString(obj.defaults + "Group_title"); } result = new SpecialSubscription(obj.url, obj.title); if ("defaults" in obj) result.defaults = obj.defaults.split(" "); } if ("fixedTitle" in obj) result._fixedTitle = (obj.fixedTitle == "true"); if ("disabled" in obj) result._disabled = (obj.disabled == "true"); return result; } /** * Class for special filter subscriptions (user's filters) * @param {String} url see Subscription() * @param {String} [title] see Subscription() * @constructor * @augments Subscription */ function SpecialSubscription(url, title) { Subscription.call(this, url, title); } SpecialSubscription.prototype = { __proto__: Subscription.prototype, /** * Filter types that should be added to this subscription by default * (entries should correspond to keys in SpecialSubscription.defaultsMap). * @type Array of String */ defaults: null, /** * Tests whether a filter should be added to this group by default * @param {Filter} filter filter to be tested * @return {Boolean} */ isDefaultFor: function(filter) { if (this.defaults && this.defaults.length) { for each (let type in this.defaults) { if (filter instanceof SpecialSubscription.defaultsMap[type]) return true; if (!(filter instanceof ActiveFilter) && type == "blacklist") return true; } } return false; }, /** * See Subscription.serialize() */ serialize: function(buffer) { Subscription.prototype.serialize.call(this, buffer); if (this.defaults && this.defaults.length) buffer.push("defaults=" + this.defaults.filter(function(type) type in SpecialSubscription.defaultsMap).join(" ")); if (this._lastDownload) buffer.push("lastDownload=" + this._lastDownload); } }; SpecialSubscription.defaultsMap = { __proto__: null, "whitelist": WhitelistFilter, "blocking": BlockingFilter, "elemhide": ElemHideFilter }; /** * Creates a new user-defined filter group. * @param {String} [title] title of the new filter group * @result {SpecialSubscription} */ SpecialSubscription.create = function(title) { let url; do { url = "~user~" + Math.round(Math.random()*1000000); } while (url in Subscription.knownSubscriptions); return new SpecialSubscription(url, title) }; /** * Creates a new user-defined filter group and adds the given filter to it. * This group will act as the default group for this filter type. */ SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSubscription*/ { let subscription = SpecialSubscription.create(); subscription.filters.push(filter); for (let type in SpecialSubscription.defaultsMap) { if (filter instanceof SpecialSubscription.defaultsMap[type]) subscription.defaults = [type]; } if (!subscription.defaults) subscription.defaults = ["blocking"]; subscription.title = Utils.getString(subscription.defaults[0] + "Group_title"); return subscription; }; /** * Abstract base class for regular filter subscriptions (both internally and externally updated) * @param {String} url see Subscription() * @param {String} [title] see Subscription() * @constructor * @augments Subscription */ function RegularSubscription(url, title) { Subscription.call(this, url, title || url); } RegularSubscription.prototype = { __proto__: Subscription.prototype, _homepage: null, _lastDownload: 0, /** * Filter subscription homepage if known * @type String */ get homepage() this._homepage, set homepage(value) { if (value != this._homepage) { let oldValue = this._homepage; this._homepage = value; FilterNotifier.triggerListeners("subscription.homepage", this, value, oldValue); } return this._homepage; }, /** * Time of the last subscription download (in seconds since the beginning of the epoch) * @type Number */ get lastDownload() this._lastDownload, set lastDownload(value) { if (value != this._lastDownload) { let oldValue = this._lastDownload; this._lastDownload = value; FilterNotifier.triggerListeners("subscription.lastDownload", this, value, oldValue); } return this._lastDownload; }, /** * See Subscription.serialize() */ serialize: function(buffer) { Subscription.prototype.serialize.call(this, buffer); if (this._homepage) buffer.push("homepage=" + this._homepage); if (this._lastDownload) buffer.push("lastDownload=" + this._lastDownload); } }; /** * Class for filter subscriptions updated by externally (by other extension) * @param {String} url see Subscription() * @param {String} [title] see Subscription() * @constructor * @augments RegularSubscription */ function ExternalSubscription(url, title) { RegularSubscription.call(this, url, title); } ExternalSubscription.prototype = { __proto__: RegularSubscription.prototype, /** * See Subscription.serialize() */ serialize: function(buffer) { throw new Error("Unexpected call, external subscriptions should not be serialized"); } }; /** * Class for filter subscriptions updated by externally (by other extension) * @param {String} url see Subscription() * @param {String} [title] see Subscription() * @constructor * @augments RegularSubscription */ function DownloadableSubscription(url, title) { RegularSubscription.call(this, url, title); } DownloadableSubscription.prototype = { __proto__: RegularSubscription.prototype, _downloadStatus: null, _lastCheck: 0, _errors: 0, /** * Next URL the downloaded should be attempted from (in case of redirects) * @type String */ nextURL: null, /** * Status of the last download (ID of a string) * @type String */ get downloadStatus() this._downloadStatus, set downloadStatus(value) { let oldValue = this._downloadStatus; this._downloadStatus = value; FilterNotifier.triggerListeners("subscription.downloadStatus", this, value, oldValue); return this._downloadStatus; }, /** * Value of the Last-Modified header returned by the server on last download * @type String */ lastModified: null, /** * Time of the last successful download (in seconds since the beginning of the * epoch). */ lastSuccess: 0, /** * Time when the subscription was considered for an update last time (in seconds * since the beginning of the epoch). This will be used to increase softExpiration * if the user doesn't use Adblock Plus for some time. * @type Number */ get lastCheck() this._lastCheck, set lastCheck(value) { if (value != this._lastCheck) { let oldValue = this._lastCheck; this._lastCheck = value; FilterNotifier.triggerListeners("subscription.lastCheck", this, value, oldValue); } return this._lastCheck; }, /** * Hard expiration time of the filter subscription (in seconds since the beginning of the epoch) * @type Number */ expires: 0, /** * Soft expiration time of the filter subscription (in seconds since the beginning of the epoch) * @type Number */ softExpiration: 0, /** * Number of download failures since last success * @type Number */ get errors() this._errors, set errors(value) { if (value != this._errors) { let oldValue = this._errors; this._errors = value; FilterNotifier.triggerListeners("subscription.errors", this, value, oldValue); } return this._errors; }, /** * Minimal Adblock Plus version required for this subscription * @type String */ requiredVersion: null, /** * Should be true if requiredVersion is higher than current Adblock Plus version * @type Boolean */ upgradeRequired: false, /** * Value of the X-Alternative-Locations header: comma-separated list of URLs * with their weighting factors, e.g.: http://foo.example.com/;q=0.5,http://bar.example.com/;q=2 * @type String */ alternativeLocations: null, /** * See Subscription.serialize() */ serialize: function(buffer) { RegularSubscription.prototype.serialize.call(this, buffer); if (this.nextURL) buffer.push("nextURL=" + this.nextURL); if (this.downloadStatus) buffer.push("downloadStatus=" + this.downloadStatus); if (this.lastModified) buffer.push("lastModified=" + this.lastModified); if (this.lastSuccess) buffer.push("lastSuccess=" + this.lastSuccess); if (this.lastCheck) buffer.push("lastCheck=" + this.lastCheck); if (this.expires) buffer.push("expires=" + this.expires); if (this.softExpiration) buffer.push("softExpiration=" + this.softExpiration); if (this.errors) buffer.push("errors=" + this.errors); if (this.requiredVersion) buffer.push("requiredVersion=" + this.requiredVersion); if (this.alternativeLocations) buffer.push("alternativeLocations=" + this.alternativeLocations); } };