/*
* This file is part of Adblock Plus ,
* Copyright (C) 2006-2017 eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* 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 .
*/
"use strict";
/**
* @fileOverview Definition of Subscription class and its subclasses.
*/
const {ActiveFilter, BlockingFilter,
WhitelistFilter, ElemHideBase} = require("filterClasses");
const {FilterNotifier} = require("filterNotifier");
const {desc, extend} = require("coreUtils");
/**
* 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 = [];
if (title)
this._title = title;
Subscription.knownSubscriptions[url] = this;
}
exports.Subscription = Subscription;
Subscription.prototype =
{
/**
* Download location of the subscription
* @type {string}
*/
url: null,
/**
* Filters contained in the filter subscription
* @type {Filter[]}
*/
filters: null,
_title: null,
_fixedTitle: false,
_disabled: false,
/**
* Title of the filter subscription
* @type {string}
*/
get title()
{
return 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()
{
return 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()
{
return 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 subscription to an array of strings for writing
* out on the disk.
* @param {string[]} buffer buffer to push the serialization results into
*/
serialize(buffer)
{
buffer.push("[Subscription]");
buffer.push("url=" + this.url);
if (this._title)
buffer.push("title=" + this._title);
if (this._fixedTitle)
buffer.push("fixedTitle=true");
if (this._disabled)
buffer.push("disabled=true");
},
serializeFilters(buffer)
{
for (let filter of this.filters)
buffer.push(filter.text.replace(/\[/g, "\\["));
},
toString()
{
let buffer = [];
this.serialize(buffer);
return buffer.join("\n");
}
};
/**
* Cache for known filter subscriptions, maps URL to subscription objects.
* @type {Object}
*/
Subscription.knownSubscriptions = Object.create(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];
if (url[0] != "~")
return new DownloadableSubscription(url, null);
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;
if (obj.url[0] != "~")
{
// URL is valid - this is a downloadable subscription
result = new DownloadableSubscription(obj.url, obj.title);
if ("downloadStatus" in obj)
result._downloadStatus = obj.downloadStatus;
if ("lastSuccess" in obj)
result.lastSuccess = parseInt(obj.lastSuccess, 10) || 0;
if ("lastCheck" in obj)
result._lastCheck = parseInt(obj.lastCheck, 10) || 0;
if ("expires" in obj)
result.expires = parseInt(obj.expires, 10) || 0;
if ("softExpiration" in obj)
result.softExpiration = parseInt(obj.softExpiration, 10) || 0;
if ("errors" in obj)
result._errors = parseInt(obj.errors, 10) || 0;
if ("version" in obj)
result.version = parseInt(obj.version, 10) || 0;
if ("requiredVersion" in obj)
result.requiredVersion = obj.requiredVersion;
if ("homepage" in obj)
result._homepage = obj.homepage;
if ("lastDownload" in obj)
result._lastDownload = parseInt(obj.lastDownload, 10) || 0;
if ("downloadCount" in obj)
result.downloadCount = parseInt(obj.downloadCount, 10) || 0;
}
else
{
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 ("privateMode" in obj)
result.privateMode = (obj.privateMode == "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);
}
exports.SpecialSubscription = SpecialSubscription;
SpecialSubscription.prototype = extend(Subscription, {
/**
* Filter types that should be added to this subscription by default
* (entries should correspond to keys in SpecialSubscription.defaultsMap).
* @type {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(filter)
{
if (this.defaults && this.defaults.length)
{
for (let type of this.defaults)
{
if (filter instanceof SpecialSubscription.defaultsMap[type])
return true;
if (!(filter instanceof ActiveFilter) && type == "blacklist")
return true;
}
}
return false;
},
/**
* See Subscription.serialize()
* @inheritdoc
*/
serialize(buffer)
{
Subscription.prototype.serialize.call(this, buffer);
if (this.defaults && this.defaults.length)
{
buffer.push("defaults=" +
this.defaults.filter(
type => type in SpecialSubscription.defaultsMap
).join(" ")
);
}
if (this._lastDownload)
buffer.push("lastDownload=" + this._lastDownload);
}
});
SpecialSubscription.defaultsMap = Object.create(null, desc({
whitelist: WhitelistFilter,
blocking: BlockingFilter,
elemhide: ElemHideBase
}));
/**
* Creates a new user-defined filter group.
* @param {string} [title] title of the new filter group
* @return {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.
* @param {Filter} filter
* @return {SpecialSubscription}
*/
SpecialSubscription.createForFilter = function(filter)
{
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"];
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);
}
exports.RegularSubscription = RegularSubscription;
RegularSubscription.prototype = extend(Subscription, {
_homepage: null,
_lastDownload: 0,
/**
* Filter subscription homepage if known
* @type {string}
*/
get homepage()
{
return 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()
{
return 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()
* @inheritdoc
*/
serialize(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 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);
}
exports.ExternalSubscription = ExternalSubscription;
ExternalSubscription.prototype = extend(RegularSubscription, {
/**
* See Subscription.serialize()
* @inheritdoc
*/
serialize(buffer)
{
throw new Error(
"Unexpected call, external subscriptions should not be serialized"
);
}
});
/**
* Class for filter subscriptions updated 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);
}
exports.DownloadableSubscription = DownloadableSubscription;
DownloadableSubscription.prototype = extend(RegularSubscription, {
_downloadStatus: null,
_lastCheck: 0,
_errors: 0,
/**
* Status of the last download (ID of a string)
* @type {string}
*/
get downloadStatus()
{
return this._downloadStatus;
},
set downloadStatus(value)
{
let oldValue = this._downloadStatus;
this._downloadStatus = value;
FilterNotifier.triggerListeners("subscription.downloadStatus",
this, value, oldValue);
return this._downloadStatus;
},
/**
* 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()
{
return 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()
{
return 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;
},
/**
* Version of the subscription data retrieved on last successful download
* @type {number}
*/
version: 0,
/**
* Minimal Adblock Plus version required for this subscription
* @type {string}
*/
requiredVersion: null,
/**
* Number indicating how often the object was downloaded.
* @type {number}
*/
downloadCount: 0,
/**
* Should be true if the Privatemode: header is set to true in the subscription
* @type Boolean
*/
privateMode: false,
/**
* See Subscription.serialize()
* @inheritdoc
*/
serialize(buffer)
{
RegularSubscription.prototype.serialize.call(this, buffer);
if (this.downloadStatus)
buffer.push("downloadStatus=" + this.downloadStatus);
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.version)
buffer.push("version=" + this.version);
if (this.requiredVersion)
buffer.push("requiredVersion=" + this.requiredVersion);
if (this.downloadCount)
buffer.push("downloadCount=" + this.downloadCount);
if (this.privateMode)
buffer.push("privateMode=" + this.privateMode);
}
});