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 */
35 var embedExceptions = {}
39 /* Purpose: Extracts and returns only the domain name from an URL
41 const getDomainName = function(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
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) {
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) {
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)) {
75 var linkDomain = getDomainName(linkNode.href)
76 if (linkDomain != sourceDomain && !(linkDomain in whiteListDomains)) {
77 linkNode.innerHTML = '[Blocked L2]'
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) }
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]' }
100 /* Purpose: Wrapper for httpChannel.getRequestHeader that doesn't crash
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
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
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
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
163 const readSetting = function(settingPath) {
164 var pathTokens = settingPath.split('.')
166 switch (pathTokens[2]) {
168 filterLevel = preferenceManager.getIntPref(settingPath)
172 Minimalist.prototype.skipRefresh = preferenceManager.getBoolPref(settingPath)
176 Minimalist.prototype.linkify = preferenceManager.getBoolPref(settingPath)
180 var destinationDomains = preferenceManager.getCharPref(settingPath).split(',')
183 for each (var destinationDomain in destinationDomains) {
184 embedExceptions[destinationDomain] = true }
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 }
197 /* Purpose: The ObserverService notifies us of all events we've subscribed to by calling this function
199 const observerCallback = function(triggeringObject, eventName, eventData) {
201 case 'http-on-examine-response':
202 filterIncomingHTTP(triggeringObject)
205 case 'http-on-modify-request':
206 filterOutgoingHTTP(triggeringObject)
209 case 'nsPref:changed':
210 readSetting(eventData)
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]) }