Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/synchronizer.js

Issue 11153017: Improve Synchronizer functionality (Closed)
Patch Set: Created July 17, 2013, 12:32 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« lib/downloader.js ('K') | « lib/subscriptionClasses.js ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /* 1 /*
2 * This file is part of Adblock Plus <http://adblockplus.org/>, 2 * This file is part of Adblock Plus <http://adblockplus.org/>,
3 * Copyright (C) 2006-2013 Eyeo GmbH 3 * Copyright (C) 2006-2013 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 /** 18 /**
19 * @fileOverview Manages synchronization of filter subscriptions. 19 * @fileOverview Manages synchronization of filter subscriptions.
20 */ 20 */
21 21
22 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 22 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
23 Cu.import("resource://gre/modules/Services.jsm"); 23 Cu.import("resource://gre/modules/Services.jsm");
24 24
25 let {TimeLine} = require("timeline"); 25 let {TimeLine} = require("timeline");
26 let {Utils} = require("utils"); 26 let {Downloader, Downloadable,
27 MILLIS_IN_SECOND, MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY} = require ("downloader");
28 let {Filter, CommentFilter} = require("filterClasses");
27 let {FilterStorage} = require("filterStorage"); 29 let {FilterStorage} = require("filterStorage");
28 let {FilterNotifier} = require("filterNotifier"); 30 let {FilterNotifier} = require("filterNotifier");
29 let {Prefs} = require("prefs"); 31 let {Prefs} = require("prefs");
30 let {Filter, CommentFilter} = require("filterClasses");
31 let {Subscription, DownloadableSubscription} = require("subscriptionClasses"); 32 let {Subscription, DownloadableSubscription} = require("subscriptionClasses");
32 33 let {Utils} = require("utils");
33 let MILLISECONDS_IN_SECOND = 1000; 34
34 let SECONDS_IN_MINUTE = 60; 35 let INITIAL_DELAY = 6 * MILLIS_IN_MINUTE;
35 let SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE; 36 let CHECK_INTERVAL = 1 * MILLIS_IN_HOUR;
36 let SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR; 37 let DEFAULT_EXPIRATION_INTERVAL = 5 * MILLIS_IN_DAY;
37 let INITIAL_DELAY = 6 * SECONDS_IN_MINUTE;
38 let CHECK_INTERVAL = SECONDS_IN_HOUR;
39 let MIN_EXPIRATION_INTERVAL = 1 * SECONDS_IN_DAY;
40 let MAX_EXPIRATION_INTERVAL = 14 * SECONDS_IN_DAY;
41 let MAX_ABSENSE_INTERVAL = 1 * SECONDS_IN_DAY;
42
43 let timer = null;
44 38
45 /** 39 /**
46 * Map of subscriptions currently being downloaded, all currently downloaded 40 * The object providing actual downloading functionality.
47 * URLs are keys of that map. 41 * @type Downloader
48 */ 42 */
49 let executing = {__proto__: null}; 43 let downloader = null;
50 44
51 /** 45 /**
52 * This object is responsible for downloading filter subscriptions whenever 46 * This object is responsible for downloading filter subscriptions whenever
53 * necessary. 47 * necessary.
54 * @class 48 * @class
55 */ 49 */
56 let Synchronizer = exports.Synchronizer = 50 let Synchronizer = exports.Synchronizer =
57 { 51 {
58 /** 52 /**
59 * Called on module startup. 53 * Called on module startup.
60 */ 54 */
61 init: function() 55 init: function()
62 { 56 {
63 TimeLine.enter("Entered Synchronizer.init()"); 57 TimeLine.enter("Entered Synchronizer.init()");
64 58
65 let callback = function() 59 downloader = new Downloader(this.getDownloadables.bind(this), INITIAL_DELAY, CHECK_INTERVAL);
66 {
67 timer.delay = CHECK_INTERVAL * MILLISECONDS_IN_SECOND;
68 checkSubscriptions();
69 };
70
71 timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
72 timer.initWithCallback(callback, INITIAL_DELAY * MILLISECONDS_IN_SECOND, Ci. nsITimer.TYPE_REPEATING_SLACK);
73 onShutdown.add(function() 60 onShutdown.add(function()
74 { 61 {
75 timer.cancel(); 62 downloader.cancel();
76 }); 63 });
77 64
65 downloader.onExpirationChange = this._onExpirationChange.bind(this);
66 downloader.onDownloadStarted = this._onDownloadStarted.bind(this);
67 downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this);
68 downloader.onDownloadError = this._onDownloadError.bind(this);
69
78 TimeLine.leave("Synchronizer.init() done"); 70 TimeLine.leave("Synchronizer.init() done");
79 }, 71 },
80 72
81 /** 73 /**
82 * Checks whether a subscription is currently being downloaded. 74 * Checks whether a subscription is currently being downloaded.
83 * @param {String} url URL of the subscription 75 * @param {String} url URL of the subscription
84 * @return {Boolean} 76 * @return {Boolean}
85 */ 77 */
86 isExecuting: function(url) 78 isExecuting: function(url)
87 { 79 {
88 return url in executing; 80 return downloader.isDownloading(url);
89 }, 81 },
90 82
91 /** 83 /**
92 * Starts the download of a subscription. 84 * Starts the download of a subscription.
93 * @param {DownloadableSubscription} subscription Subscription to be download ed 85 * @param {DownloadableSubscription} subscription Subscription to be download ed
94 * @param {Boolean} manual true for a manually started download (should not t rigger fallback requests) 86 * @param {Boolean} manual true for a manually started download (should not t rigger fallback requests)
95 * @param {Boolean} forceDownload if true, the subscription will even be red ownloaded if it didn't change on the server 87 */
96 */ 88 execute: function(subscription, manual)
97 execute: function(subscription, manual, forceDownload) 89 {
98 { 90 downloader.download(this.getDownloadable(subscription, manual));
99 // Delay execution, SeaMonkey 2.1 won't fire request's event handlers 91 },
100 // otherwise if the window that called us is closed. 92
101 Utils.runAsync(this.executeInternal, this, subscription, manual, forceDownlo ad); 93 /**
102 }, 94 * Yields Downloadable instances for all subscriptions that can be downloaded.
103 95 */
104 executeInternal: function(subscription, manual, forceDownload) 96 getDownloadables: function()
105 { 97 {
106 let url = subscription.url; 98 if (!Prefs.subscriptions_autoupdate)
107 if (url in executing)
108 return; 99 return;
109 100
110 let newURL = subscription.nextURL; 101 for each (let subscription in FilterStorage.subscriptions)
111 let hadTemporaryRedirect = false; 102 {
112 subscription.nextURL = null; 103 if (subscription instanceof DownloadableSubscription)
113 104 yield this.getDownloadable(subscription, false);
114 let loadFrom = newURL; 105 }
115 let isBaseLocation = true; 106 },
116 if (!loadFrom) 107
117 loadFrom = url; 108 /**
118 if (loadFrom == url) 109 * Creates a Downloadable instance for a subscription.
119 { 110 */
120 if (subscription.alternativeLocations) 111 getDownloadable: function(/**Subscription*/ subscription, /**Boolean*/ manual) /**Downloadable*/
112 {
113 let result = new Downloadable(subscription.url);
114 if (subscription.lastDownload != subscription.lastSuccess)
115 result.lastError = subscription.lastDownload * MILLIS_IN_SECOND;
116 result.lastCheck = subscription.lastCheck * MILLIS_IN_SECOND;
117 result.lastVersion = subscription.version;
118 result.softExpiration = subscription.softExpiration * MILLIS_IN_SECOND;
119 result.hardExpiration = subscription.expires * MILLIS_IN_SECOND;
120 result.manual = manual;
121 return result;
122 },
123
124 _onExpirationChange: function(downloadable)
125 {
126 let subscription = Subscription.fromURL(downloadable.url);
127 subscription.lastCheck = Math.round(downloadable.lastCheck / MILLIS_IN_SECON D);
128 subscription.softExpiration = Math.round(downloadable.softExpiration / MILLI S_IN_SECOND);
129 subscription.expires = Math.round(downloadable.hardExpiration / MILLIS_IN_SE COND);
130 },
131
132 _onDownloadStarted: function(downloadable)
133 {
134 let subscription = Subscription.fromURL(downloadable.url);
135 FilterNotifier.triggerListeners("subscription.downloadStatus", subscription) ;
136 },
137
138 _onDownloadSuccess: function(downloadable, responseText, errorCallback, redire ctCallback)
139 {
140 let lines = responseText.split(/[\r\n]+/);
141 let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]);
142 if (!match)
143 return errorCallback("synchronize_invalid_data");
144 let minVersion = match[1];
145
146 // Don't remove parameter comments immediately but add them to a list first,
147 // they need to be considered in the checksum calculation.
148 let remove = [];
149 let params = {
150 redirect: null,
151 homepage: null,
152 title: null,
153 version: null,
154 expires: null
155 };
156 for (let i = 0; i < lines.length; i++)
157 {
158 let match = /^\s*!\s*(\w+)\s*:\s*(.*)/.exec(lines[i]);
159 if (match)
121 { 160 {
122 // We have alternative download locations, choose one. "Regular" 161 let keyword = match[1].toLowerCase();
123 // subscription URL always goes in with weight 1. 162 let value = match[2];
124 let options = [[1, url]]; 163 if (keyword in params)
125 let totalWeight = 1;
126 for each (let alternative in subscription.alternativeLocations.split(',' ))
127 { 164 {
128 if (!/^https?:\/\//.test(alternative)) 165 params[keyword] = value;
129 continue; 166 remove.push(i);
130
131 let weight = 1;
132 let match = /;q=([\d\.]+)$/.exec(alternative);
133 if (match)
134 {
135 weight = parseFloat(match[1]);
136 if (isNaN(weight) || !isFinite(weight) || weight < 0)
137 weight = 1;
138 if (weight > 10)
139 weight = 10;
140
141 alternative = alternative.substr(0, match.index);
142 }
143 options.push([weight, alternative]);
144 totalWeight += weight;
145 } 167 }
146 168 else if (keyword == "checksum")
147 let choice = Math.random() * totalWeight;
148 for each (let [weight, alternative] in options)
149 { 169 {
150 choice -= weight; 170 lines.splice(i--, 1);
151 if (choice < 0) 171 let checksum = Utils.generateChecksum(lines);
152 { 172 if (checksum && checksum != value.replace(/=+$/, ""))
153 loadFrom = alternative; 173 return errorCallback("synchronize_checksum_mismatch");
154 break;
155 }
156 }
157
158 isBaseLocation = (loadFrom == url);
159 }
160 }
161 else
162 {
163 // Ignore modification date if we are downloading from a different locatio n
164 forceDownload = true;
165 }
166
167 let {addonVersion} = require("info");
168 loadFrom = loadFrom.replace(/%VERSION%/, "ABP" + addonVersion);
169
170 let request = null;
171 let errorCallback = function(error)
172 {
173 let channelStatus = -1;
174 try
175 {
176 channelStatus = request.channel.status;
177 } catch (e) {}
178 let responseStatus = "";
179 try
180 {
181 responseStatus = request.channel.QueryInterface(Ci.nsIHttpChannel).respo nseStatus;
182 } catch (e) {}
183 setError(subscription, error, channelStatus, responseStatus, loadFrom, isB aseLocation, manual);
184 };
185
186 try
187 {
188 request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci. nsIXMLHttpRequest);
189 request.mozBackgroundRequest = true;
190 request.open("GET", loadFrom);
191 }
192 catch (e)
193 {
194 errorCallback("synchronize_invalid_url");
195 return;
196 }
197
198 try {
199 request.overrideMimeType("text/plain");
200 request.channel.loadFlags = request.channel.loadFlags |
201 request.channel.INHIBIT_CACHING |
202 request.channel.VALIDATE_ALWAYS;
203
204 // Override redirect limit from preferences, user might have set it to 1
205 if (request.channel instanceof Ci.nsIHttpChannel)
206 request.channel.redirectionLimit = 5;
207
208 var oldNotifications = request.channel.notificationCallbacks;
209 var oldEventSink = null;
210 request.channel.notificationCallbacks =
211 {
212 QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIC hannelEventSink]),
213
214 getInterface: function(iid)
215 {
216 if (iid.equals(Ci.nsIChannelEventSink))
217 {
218 try {
219 oldEventSink = oldNotifications.QueryInterface(iid);
220 } catch(e) {}
221 return this;
222 }
223
224 if (oldNotifications)
225 return oldNotifications.QueryInterface(iid);
226 else
227 throw Cr.NS_ERROR_NO_INTERFACE;
228 },
229
230 asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback )
231 {
232 if (isBaseLocation && !hadTemporaryRedirect && oldChannel instanceof C i.nsIHttpChannel)
233 {
234 try
235 {
236 subscription.alternativeLocations = oldChannel.getResponseHeader(" X-Alternative-Locations");
237 }
238 catch (e)
239 {
240 subscription.alternativeLocations = null;
241 }
242 }
243
244 if (flags & Ci.nsIChannelEventSink.REDIRECT_TEMPORARY)
245 hadTemporaryRedirect = true;
246 else if (!hadTemporaryRedirect)
247 newURL = newChannel.URI.spec;
248
249 if (oldEventSink)
250 oldEventSink.asyncOnChannelRedirect(oldChannel, newChannel, flags, c allback);
251 else
252 callback.onRedirectVerifyCallback(Cr.NS_OK);
253 } 174 }
254 } 175 }
255 } 176 }
256 catch (e) 177
257 { 178 if (params.redirect)
258 Cu.reportError(e) 179 return redirectCallback(params.redirect);
259 } 180
260 181 // Handle redirects
261 if (subscription.lastModified && !forceDownload) 182 let subscription = Subscription.fromURL(downloadable.redirectURL || download able.url);
262 request.setRequestHeader("If-Modified-Since", subscription.lastModified); 183 if (downloadable.redirectURL && downloadable.redirectURL != downloadable.url )
263 184 {
264 request.addEventListener("error", function(ev) 185 let oldSubscription = Subscription.fromURL(downloadable.url);
265 { 186 subscription.title = oldSubscription.title;
266 if (onShutdown.done) 187 subscription.disabled = oldSubscription.disabled;
267 return; 188 subscription.lastCheck = oldSubscription.lastCheck;
268 189
269 delete executing[url]; 190 let listed = (oldSubscription.url in FilterStorage.knownSubscriptions);
270 try { 191 if (listed)
271 request.channel.notificationCallbacks = null; 192 FilterStorage.removeSubscription(oldSubscription);
272 } catch (e) {} 193
273 194 delete Subscription.knownSubscriptions[oldSubscription.url];
274 errorCallback("synchronize_connection_error"); 195
275 }, false); 196 if (listed)
276 197 FilterStorage.addSubscription(subscription);
277 request.addEventListener("load", function(ev) 198 }
278 { 199
279 if (onShutdown.done) 200 // The download actually succeeded
280 return; 201 subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND);
281 202 subscription.downloadStatus = "synchronize_ok";
282 delete executing[url]; 203 subscription.errors = 0;
283 try { 204
284 request.channel.notificationCallbacks = null; 205 // Remove lines containing parameters
285 } catch (e) {} 206 for (let i = remove.length - 1; i >= 0; i--)
286 207 lines.splice(remove[i], 1);
287 // Status will be 0 for non-HTTP requests 208
288 if (request.status && request.status != 200 && request.status != 304) 209 // Process parameters
210 if (params.homepage)
211 {
212 let uri = Utils.makeURI(params.homepage);
213 if (uri && (uri.scheme == "http" || uri.scheme == "https"))
214 subscription.homepage = uri.spec;
215 }
216
217 if (params.title)
218 {
219 subscription.title = params.title;
220 subscription.fixedTitle = true;
221 }
222 else
223 subscription.fixedTitle = false;
224
225 subscription.version = (params.version ? parseInt(params.version, 10) : 0);
226
227 let expirationInterval = DEFAULT_EXPIRATION_INTERVAL;
228 if (params.expires)
229 {
230 let match = /^(\d+)\s*(h)?/.exec(params.expires);
231 if (match)
289 { 232 {
290 errorCallback("synchronize_connection_error"); 233 let interval = parseInt(match[1], 10);
291 return; 234 if (match[2])
235 expirationInterval = interval * MILLIS_IN_HOUR;
236 else
237 expirationInterval = interval * MILLIS_IN_DAY;
292 } 238 }
293 239 }
294 let newFilters = null; 240
295 if (request.status != 304) 241 let [softExpiration, hardExpiration] = downloader.processExpirationInterval( expirationInterval);
242 subscription.softExpiration = Math.round(softExpiration / MILLIS_IN_SECOND);
243 subscription.expires = Math.round(hardExpiration / MILLIS_IN_SECOND);
244
245 delete subscription.requiredVersion;
246 delete subscription.upgradeRequired;
247 if (minVersion)
248 {
249 let {addonVersion} = require("info");
250 subscription.requiredVersion = minVersion;
251 if (Services.vc.compare(minVersion, addonVersion) > 0)
252 subscription.upgradeRequired = true;
253 }
254
255 // Process filters
256 lines.shift();
257 let filters = [];
258 for each (let line in lines)
259 {
260 line = Filter.normalize(line);
261 if (line)
262 filters.push(Filter.fromText(line));
263 }
264
265 FilterStorage.updateSubscriptionFilters(subscription, filters);
266
267 return undefined;
268 },
269
270 _onDownloadError: function(downloadable, downloadURL, error, channelStatus, re sponseStatus, redirectCallback)
271 {
272 let subscription = Subscription.fromURL(downloadable.url);
273 subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND);
274 subscription.downloadStatus = error;
275
276 // Request fallback URL if necessary - for automatic updates only
277 if (!downloadable.manual)
278 {
279 subscription.errors++;
280
281 if (redirectCallback && subscription.errors >= Prefs.subscriptions_fallbac kerrors && /^https?:\/\//i.test(subscription.url))
296 { 282 {
297 newFilters = readFilters(subscription, request.responseText, errorCallba ck); 283 subscription.errors = 0;
298 if (!newFilters) 284
299 return; 285 let fallbackURL = Prefs.subscriptions_fallbackurl;
300 286 let {addonVersion} = require("info");
301 subscription.lastModified = request.getResponseHeader("Last-Modified"); 287 fallbackURL = fallbackURL.replace(/%VERSION%/g, encodeURIComponent(addon Version));
288 fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent( subscription.url));
289 fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadU RL));
290 fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error)) ;
291 fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent (channelStatus));
292 fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponen t(responseStatus));
293
294 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstan ce(Ci.nsIXMLHttpRequest);
295 request.mozBackgroundRequest = true;
296 request.open("GET", fallbackURL);
297 request.overrideMimeType("text/plain");
298 request.channel.loadFlags = request.channel.loadFlags |
299 request.channel.INHIBIT_CACHING |
300 request.channel.VALIDATE_ALWAYS;
301 request.addEventListener("load", function(ev)
302 {
303 if (onShutdown.done)
304 return;
305
306 if (!(subscription.url in FilterStorage.knownSubscriptions))
307 return;
308
309 let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText);
310 if (match && match[1] == "301" && match[2] && /^https?:\/\//i.test(mat ch[2])) // Moved permanently
311 redirectCallback(match[2]);
312 else if (match && match[1] == "410") // Gone
313 {
314 let data = "[Adblock]\n" + subscription.filters.map(function(f) f.te xt).join("\n");
315 redirectCallback("data:text/plain," + encodeURIComponent(data));
316 }
317 }, false);
318 request.send(null);
302 } 319 }
303 320 }
304 if (isBaseLocation && !hadTemporaryRedirect) 321 },
305 subscription.alternativeLocations = request.getResponseHeader("X-Alterna tive-Locations");
306 subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now () / MILLISECONDS_IN_SECOND);
307 subscription.downloadStatus = "synchronize_ok";
308 subscription.errors = 0;
309
310 // Expiration header is relative to server time - use Date header if it ex ists, otherwise local time
311 let now = Math.round((new Date(request.getResponseHeader("Date")).getTime( ) || Date.now()) / MILLISECONDS_IN_SECOND);
312 let expires = Math.round(new Date(request.getResponseHeader("Expires")).ge tTime() / MILLISECONDS_IN_SECOND) || 0;
313 let expirationInterval = (expires ? expires - now : 0);
314 for each (let filter in newFilters || subscription.filters)
315 {
316 if (!(filter instanceof CommentFilter))
317 continue;
318
319 let match = /\bExpires\s*(?::|after)\s*(\d+)\s*(h)?/i.exec(filter.text);
320 if (match)
321 {
322 let interval = parseInt(match[1], 10);
323 if (match[2])
324 interval *= SECONDS_IN_HOUR;
325 else
326 interval *= SECONDS_IN_DAY;
327
328 if (interval > expirationInterval)
329 expirationInterval = interval;
330 }
331 }
332
333 // Expiration interval should be within allowed range
334 expirationInterval = Math.min(Math.max(expirationInterval, MIN_EXPIRATION_ INTERVAL), MAX_EXPIRATION_INTERVAL);
335
336 // Hard expiration: download immediately after twice the expiration interv al
337 subscription.expires = (subscription.lastDownload + expirationInterval * 2 );
338
339 // Soft expiration: use random interval factor between 0.8 and 1.2
340 subscription.softExpiration = (subscription.lastDownload + Math.round(expi rationInterval * (Math.random() * 0.4 + 0.8)));
341
342 // Process some special filters and remove them
343 if (newFilters)
344 {
345 let fixedTitle = false;
346 for (let i = 0; i < newFilters.length; i++)
347 {
348 let filter = newFilters[i];
349 if (!(filter instanceof CommentFilter))
350 continue;
351
352 let match = /^!\s*(\w+)\s*:\s*(.*)/.exec(filter.text);
353 if (match)
354 {
355 let keyword = match[1].toLowerCase();
356 let value = match[2];
357 let known = true;
358 if (keyword == "redirect")
359 {
360 if (isBaseLocation && value != url)
361 subscription.nextURL = value;
362 }
363 else if (keyword == "homepage")
364 {
365 let uri = Utils.makeURI(value);
366 if (uri && (uri.scheme == "http" || uri.scheme == "https"))
367 subscription.homepage = uri.spec;
368 }
369 else if (keyword == "title")
370 {
371 if (value)
372 {
373 subscription.title = value;
374 fixedTitle = true;
375 }
376 }
377 else
378 known = false;
379
380 if (known)
381 newFilters.splice(i--, 1);
382 }
383 }
384 subscription.fixedTitle = fixedTitle;
385 }
386
387 if (isBaseLocation && newURL && newURL != url)
388 {
389 let listed = (subscription.url in FilterStorage.knownSubscriptions);
390 if (listed)
391 FilterStorage.removeSubscription(subscription);
392
393 url = newURL;
394
395 let newSubscription = Subscription.fromURL(url);
396 for (let key in newSubscription)
397 delete newSubscription[key];
398 for (let key in subscription)
399 newSubscription[key] = subscription[key];
400
401 delete Subscription.knownSubscriptions[subscription.url];
402 newSubscription.oldSubscription = subscription;
403 subscription = newSubscription;
404 subscription.url = url;
405
406 if (!(subscription.url in FilterStorage.knownSubscriptions) && listed)
407 FilterStorage.addSubscription(subscription);
408 }
409
410 if (newFilters)
411 FilterStorage.updateSubscriptionFilters(subscription, newFilters);
412 delete subscription.oldSubscription;
413 }, false);
414
415 executing[url] = true;
416 FilterNotifier.triggerListeners("subscription.downloadStatus", subscription) ;
417
418 try
419 {
420 request.send(null);
421 }
422 catch (e)
423 {
424 delete executing[url];
425 errorCallback("synchronize_connection_error");
426 return;
427 }
428 }
429 }; 322 };
430 Synchronizer.init(); 323 Synchronizer.init();
431
432 /**
433 * Checks whether any subscriptions need to be downloaded and starts the downloa d
434 * if necessary.
435 */
436 function checkSubscriptions()
437 {
438 if (!Prefs.subscriptions_autoupdate)
439 return;
440
441 let time = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
442 for each (let subscription in FilterStorage.subscriptions)
443 {
444 if (!(subscription instanceof DownloadableSubscription))
445 continue;
446
447 if (subscription.lastCheck && time - subscription.lastCheck > MAX_ABSENSE_IN TERVAL)
448 {
449 // No checks for a long time interval - user must have been offline, e.g.
450 // during a weekend. Increase soft expiration to prevent load peaks on the
451 // server.
452 subscription.softExpiration += time - subscription.lastCheck;
453 }
454 subscription.lastCheck = time;
455
456 // Sanity check: do expiration times make sense? Make sure people changing
457 // system clock don't get stuck with outdated subscriptions.
458 if (subscription.expires - time > MAX_EXPIRATION_INTERVAL)
459 subscription.expires = time + MAX_EXPIRATION_INTERVAL;
460 if (subscription.softExpiration - time > MAX_EXPIRATION_INTERVAL)
461 subscription.softExpiration = time + MAX_EXPIRATION_INTERVAL;
462
463 if (subscription.softExpiration > time && subscription.expires > time)
464 continue;
465
466 // Do not retry downloads more often than MIN_EXPIRATION_INTERVAL
467 if (time - subscription.lastDownload >= MIN_EXPIRATION_INTERVAL)
468 Synchronizer.execute(subscription, false);
469 }
470 }
471
472 /**
473 * Extracts a list of filters from text returned by a server.
474 * @param {DownloadableSubscription} subscription subscription the info should be placed into
475 * @param {String} text server response
476 * @param {Function} errorCallback function to be called on error
477 * @return {Array of Filter}
478 */
479 function readFilters(subscription, text, errorCallback)
480 {
481 let lines = text.split(/[\r\n]+/);
482 let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]);
483 if (!match)
484 {
485 errorCallback("synchronize_invalid_data");
486 return null;
487 }
488 let minVersion = match[1];
489
490 for (let i = 0; i < lines.length; i++)
491 {
492 let match = /!\s*checksum[\s\-:]+([\w\+\/]+)/i.exec(lines[i]);
493 if (match)
494 {
495 lines.splice(i, 1);
496 let checksum = Utils.generateChecksum(lines);
497
498 if (checksum && checksum != match[1])
499 {
500 errorCallback("synchronize_checksum_mismatch");
501 return null;
502 }
503
504 break;
505 }
506 }
507
508 delete subscription.requiredVersion;
509 delete subscription.upgradeRequired;
510 if (minVersion)
511 {
512 let {addonVersion} = require("info");
513 subscription.requiredVersion = minVersion;
514 if (Services.vc.compare(minVersion, addonVersion) > 0)
515 subscription.upgradeRequired = true;
516 }
517
518 lines.shift();
519 let result = [];
520 for each (let line in lines)
521 {
522 line = Filter.normalize(line);
523 if (line)
524 result.push(Filter.fromText(line));
525 }
526
527 return result;
528 }
529
530 /**
531 * Handles an error during a subscription download.
532 * @param {DownloadableSubscription} subscription subscription that failed to d ownload
533 * @param {Integer} channelStatus result code of the download channel
534 * @param {String} responseStatus result code as received from server
535 * @param {String} downloadURL the URL used for download
536 * @param {String} error error ID in global.properties
537 * @param {Boolean} isBaseLocation false if the subscription was downloaded from a location specified in X-Alternative-Locations header
538 * @param {Boolean} manual true for a manually started download (should not tri gger fallback requests)
539 */
540 function setError(subscription, error, channelStatus, responseStatus, downloadUR L, isBaseLocation, manual)
541 {
542 // If download from an alternative location failed, reset the list of
543 // alternative locations - have to get an updated list from base location.
544 if (!isBaseLocation)
545 subscription.alternativeLocations = null;
546
547 try {
548 Cu.reportError("Adblock Plus: Downloading filter subscription " + subscripti on.title + " failed (" + Utils.getString(error) + ")\n" +
549 "Download address: " + downloadURL + "\n" +
550 "Channel status: " + channelStatus + "\n" +
551 "Server response: " + responseStatus);
552 } catch(e) {}
553
554 subscription.lastDownload = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
555 subscription.downloadStatus = error;
556
557 // Request fallback URL if necessary - for automatic updates only
558 if (!manual)
559 {
560 if (error == "synchronize_checksum_mismatch")
561 {
562 // No fallback for successful download with checksum mismatch, reset error counter
563 subscription.errors = 0;
564 }
565 else
566 subscription.errors++;
567
568 if (subscription.errors >= Prefs.subscriptions_fallbackerrors && /^https?:\/ \//i.test(subscription.url))
569 {
570 subscription.errors = 0;
571
572 let fallbackURL = Prefs.subscriptions_fallbackurl;
573 let {addonVersion} = require("info");
574 fallbackURL = fallbackURL.replace(/%VERSION%/g, encodeURIComponent(addonVe rsion));
575 fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent(su bscription.url));
576 fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadURL ));
577 fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error));
578 fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent(c hannelStatus));
579 fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponent( responseStatus));
580
581 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance (Ci.nsIXMLHttpRequest);
582 request.mozBackgroundRequest = true;
583 request.open("GET", fallbackURL);
584 request.overrideMimeType("text/plain");
585 request.channel.loadFlags = request.channel.loadFlags |
586 request.channel.INHIBIT_CACHING |
587 request.channel.VALIDATE_ALWAYS;
588 request.addEventListener("load", function(ev)
589 {
590 if (onShutdown.done)
591 return;
592
593 if (!(subscription.url in FilterStorage.knownSubscriptions))
594 return;
595
596 let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText);
597 if (match && match[1] == "301" && match[2]) // Moved permanently
598 subscription.nextURL = match[2];
599 else if (match && match[1] == "410") // Gone
600 {
601 let data = "[Adblock]\n" + subscription.filters.map(function(f) f.text ).join("\n");
602 let url = "data:text/plain," + encodeURIComponent(data);
603 let newSubscription = Subscription.fromURL(url);
604 newSubscription.title = subscription.title;
605 newSubscription.disabled = subscription.disabled;
606 FilterStorage.removeSubscription(subscription);
607 FilterStorage.addSubscription(newSubscription);
608 Synchronizer.execute(newSubscription);
609 }
610 }, false);
611 request.send(null);
612 }
613 }
614 }
OLDNEW
« lib/downloader.js ('K') | « lib/subscriptionClasses.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld