Version 0.6.2
[minimalist.git] / components / minimalist.js
blob35e9445d3d57b718f931270911f547a85f8ed7ef
1 function NSGetModule() {
2   /* Providing shortcuts for many otherwise lengthy XPCOM interfaces and constants */
3   const ACCEPT             = Components.interfaces.nsIContentPolicy.ACCEPT
4   const REJECT             = Components.interfaces.nsIContentPolicy.REJECT_SERVER
5   const nsIAnchorElement   = Components.interfaces.nsIDOMHTMLAnchorElement
6   const nsIContentPolicy   = Components.interfaces.nsIContentPolicy
7   const nsIEmbedElement    = Components.interfaces.nsIDOMHTMLEmbedElement
8   const nsIFrameElement    = Components.interfaces.nsIDOMHTMLFrameElement
9   const nsIHttpChannel     = Components.interfaces.nsIHttpChannel
10   const nsIImageElement    = Components.interfaces.nsIDOMHTMLImageElement
11   const nsIObjectElement   = Components.interfaces.nsIDOMHTMLObjectElement
12   const nsIObserver        = Components.interfaces.nsIObserver
13   const nsIObserverService = Components.interfaces.nsIObserverService
14   const nsIPrefBranch      = Components.interfaces.nsIPrefBranch
15   const nsIPrefBranch2     = Components.interfaces.nsIPrefBranch2
16   const nsIURI             = Components.interfaces.nsIURI
17   const nsIXULElement      = Components.interfaces.nsIDOMXULElement
19   /* Generic constants */
20   const tagsToClear       = ['Age', 'Cache-Control', 'Date', 'ETag', 'Last-Modified', 'Pragma', 'Vary']
21   const allowedSchemes    = {'chrome': true, 'resource': true}
22   const urlParser         = Components.classes['@mozilla.org/network/standard-url;1'].getService(nsIURI)
23   const preferenceManager = Components.classes['@mozilla.org/preferences-service;1'].getService(nsIPrefBranch)
24   const observerService   = Components.classes['@mozilla.org/observer-service;1'].getService(nsIObserverService)
26   /* Various regular expressions. They are automatically compiled once on startup due to the literal notation */
27   const isHTMLDocument    = /^(?:text\/html|application\/xhtml\+xml)/
28   const mustBeCached      = /^(?:text|image|multipart)\/|^application\/(?:x-)?(?:javascript|json)/
29   const extractDomainName = /([^.]+)\.[^\d.]+$/
30   const isAnImageFileName = /\.(?:jpe?g|png|gif|bmp)$/i
31   const advertBlacklist   = /(?:^|[\/.])ad(?:vert|serv(?:er)?)?s?\d?(?:[\/.]|$)/i
33   /* These variables hold the settings used throughout this extension */
34   var filterLevel         = 3
35   var embedExceptions     = {}
36   var whiteList           = {}
39   /* Purpose: Extracts and returns only the domain name from an URL
40    */
41   const getDomainName = function(URL) {
42     urlParser.spec = URL
43     return (extractDomainName.exec(urlParser.host) || {1: URL})[1].toLowerCase() }
46   /* Purpose: Has three different filter levels that try to prevent advertisement and annoying scripts from loading.
47    *          resource:// and chrome:// URLs and XUL elements are always granted to load, so the UI doesn't break
48    */
49   const filterLoadRequest = function(unused, destinationURL, sourceURL, triggeringNode) {
50     if (filterLevel < 1 || !sourceURL || triggeringNode instanceof nsIXULElement ||
51         sourceURL.scheme in allowedSchemes || destinationURL.scheme in allowedSchemes) {
52       return ACCEPT }
54     // When checking whether a resource is external, we always only compare the domain names
55     var sourceDomain      = getDomainName(sourceURL.spec)
56     var destinationDomain = getDomainName(destinationURL.spec)
57     var whiteListDomains  = whiteList[sourceDomain] || {}
59     // Filter level 1: Embed and object tags have an own whitelist that's valid for any source domain
60     if ((triggeringNode instanceof nsIEmbedElement || triggeringNode instanceof nsIObjectElement) &&
61          destinationDomain in embedExceptions) {
62       return ACCEPT }
64     // Determine wheter the triggering node is an image linked to a valid target
65     var linkNode          = triggeringNode.parentNode
66     var isLinkedImageNode = triggeringNode instanceof nsIImageElement && linkNode instanceof nsIAnchorElement &&
67                             linkNode.href.substr(0, 11) != 'javascript:'
69     // Filter level 2: Denies to load if the triggering node is an image,
70     // but does link to some other external resource, that is not an image
71     if (filterLevel > 1 && isLinkedImageNode) {
72       if (isAnImageFileName.test(linkNode.href)) {
73         return ACCEPT }
75       var linkDomain = getDomainName(linkNode.href)
76       if (linkDomain != sourceDomain && !(linkDomain in whiteListDomains)) {
77         linkNode.innerHTML = '[Blocked L2]'
78         return REJECT } }
80     // Filter level 1: Denies to load any external resource that aren't on this domain's whiteList
81     if (sourceDomain != destinationDomain && !(destinationDomain in whiteListDomains)) {
82       if (triggeringNode instanceof nsIImageElement) {
83         linkNode             = triggeringNode.ownerDocument.createElement('a')
84         linkNode.href        = triggeringNode.src
85         linkNode.textContent = '[Blocked L1]'
86         triggeringNode.parentNode.replaceChild(linkNode, triggeringNode) }
87       return REJECT }
89     // Filter level 3: This is the last measure. Checks the destination URL for blacklisted tokens
90     if (filterLevel > 2 && (advertBlacklist.test(destinationURL.spec) ||
91             isLinkedImageNode && advertBlacklist.test(linkNode.href))) {
92       if (isLinkedImageNode) {
93         linkNode.innerHTML = '[Blocked L3]' }
94       return REJECT }
96     // Fall-through case
97     return ACCEPT }
100   /* Purpose: Wrapper for httpChannel.getRequestHeader that doesn't crash
101    */
102   const getRequestHeader = function(httpChannel, tagName) {
103     try       { return httpChannel.getRequestHeader(tagName) }
104     catch (e) { return false } }
107   /* Purpose: Wrapper for httpChannel.getResponseHeader that doesn't crash
108    */
109   const getResponseHeader = function(httpChannel, tagName) {
110     try       { return httpChannel.getResponseHeader(tagName) }
111     catch (e) { return false } }
114   /* Purpose: Removes all known HTTP tags related to caching and user tracking;
115    *          inserts it an Expiry tag afterwards to enforce our own caching rules
116    *   Notes: httpChannel is of type nsISupports and must be casted to nsIHttpChannel
117    */
118   const filterIncomingHTTP = function(httpChannel) {
119     var httpChannel = httpChannel.QueryInterface(nsIHttpChannel)
121     for each (var tagName in tagsToClear) {
122       httpChannel.setResponseHeader(tagName, '', false) }
124     // This is the fall-through case
125     var expiryDate = 'Thu, 01 Jan 1970 00:00:00 GMT'
127     // (X)HTML documents will get cached for 2 minutes before being rechecked
128     var contentType = getResponseHeader(httpChannel, 'Content-Type') || ''
129     if (isHTMLDocument.test(contentType)) {
130       expiryDate = new Date( Date.now() + (2*60*1000) ).toGMTString() }
132     // Content with MIME type text/*, image/* or multipart/* will be cached forever if known to be smaller than 2 MiB
133     else if ((contentLength = getResponseHeader(httpChannel, 'Content-Length')) &&
134              parseInt(contentLength) <= (2*1024*1024) || mustBeCached.test(contentType)) {
135       expiryDate = 'Sun, 07 Feb 2106 06:28:15 GMT' }
137     httpChannel.setResponseHeader('Expires', expiryDate, false)
138     httpChannel.setResponseHeader('Last-Modified', new Date().toGMTString(), false) }
141   /* Purpose: Overwrites the User-Agent and Referer HTTP tags to archive better privacy
142    *   Notes: httpChannel is of type nsISupports and must be casted to nsIHttpChannel
143    */
144   const filterOutgoingHTTP = function(httpChannel) {
145     var httpChannel = httpChannel.QueryInterface(nsIHttpChannel)
147     // If the Referer's domain isn't the same as the target domain, make it so, but leave it untouched if it is
148     var referer       = getRequestHeader(httpChannel, 'Referer') || ''
149     var domain        = getRequestHeader(httpChannel, 'Host')    || ''
150     var refererDomain = getDomainName(referer)
151     var currentDomain = getDomainName('http://' + domain)
153     if (refererDomain != currentDomain) {
154       httpChannel.setRequestHeader('Referer', 'http://' + domain, false) }
156     // This is the User-Agent sent by the official Firefox 3.0.6 build when running on Windows XP
157     httpChannel.setRequestHeader('User-Agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.6) '
158                                              + 'Gecko/2009011913 Firefox/3.0.6 ', false) }
161   /* Purpose: Reads the new value pointed to by settingPath into a local variable
162    */
163   const readSetting = function(settingPath) {
164     var pathTokens = settingPath.split('.')
166     switch (pathTokens[2]) {
167       case 'level':
168         filterLevel = preferenceManager.getIntPref(settingPath)
169         break
171       case 'skipRefresh':
172         Minimalist.prototype.skipRefresh = preferenceManager.getBoolPref(settingPath)
173         break
175       case 'linkify':
176         Minimalist.prototype.linkify = preferenceManager.getBoolPref(settingPath)
177         break
179       case 'embed':
180         var destinationDomains = preferenceManager.getCharPref(settingPath).split(',')
181         embedExceptions = {}
183         for each (var destinationDomain in destinationDomains) {
184           embedExceptions[destinationDomain] = true }
185         break
187       case 'whitelist':
188         var sourceDomain       = pathTokens[3]
189         var destinationDomains = preferenceManager.getCharPref(settingPath).split(',')
190         whiteList[sourceDomain] = {}
192         for each (var destinationDomain in destinationDomains) {
193           whiteList[sourceDomain][destinationDomain] = true }
194         break } }
197   /* Purpose: The ObserverService notifies us of all events we've subscribed to by calling this function
198    */
199   const observerCallback = function(triggeringObject, eventName, eventData) {
200     switch (eventName) {
201       case 'http-on-examine-response':
202         filterIncomingHTTP(triggeringObject)
203         break
205       case 'http-on-modify-request':
206         filterOutgoingHTTP(triggeringObject)
207         break
209       case 'nsPref:changed':
210         readSetting(eventData)
211         break } }
214   // Prepare for XPCOM registration
215   Components.utils.import('resource://gre/modules/XPCOMUtils.jsm')
216   const Minimalist = function() { this.wrappedJSObject = this }
217   Minimalist.prototype = {
218      classDescription: 'Minimalist',
219            contractID: '@zelgadis.jp/minimalist;1',
220               classID: Components.ID('{124b1de7-bd8a-4a41-a04a-9d40070a0b3a}'),
221        QueryInterface: XPCOMUtils.generateQI([nsIContentPolicy]),
222     _xpcom_categories: [{category: 'content-policy'}],
223            shouldLoad: filterLoadRequest,
224         shouldProcess: function() { return ACCEPT } }
226   // Adding the nsIPrefBranch2 interface gives us access to the getChildList method
227   var registrar = { observe: observerCallback }
228   preferenceManager.QueryInterface(nsIPrefBranch2)
229   preferenceManager.addObserver('extensions.minimalist.', registrar, false)
230   observerService.addObserver(registrar, 'http-on-examine-response', false)
231   observerService.addObserver(registrar, 'http-on-modify-request',   false)
233   // Populate all settings with their initial values so they can be accessed right away
234   var whiteListEntries = preferenceManager.getChildList('extensions.minimalist.whitelist', {})
235   for each (whiteListEntry in whiteListEntries) { readSetting(whiteListEntry) }
236   readSetting('extensions.minimalist.level')
237   readSetting('extensions.minimalist.skipRefresh')
238   readSetting('extensions.minimalist.linkify')
239   readSetting('extensions.minimalist.embed')
241   return XPCOMUtils.generateModule([Minimalist]) }