Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
1 /* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 this.EXPORTED_SYMBOLS = [ "DownloadUtils" ];
8 /**
9 * This module provides the DownloadUtils object which contains useful methods
10 * for downloads such as displaying file sizes, transfer times, and download
11 * locations.
12 *
13 * List of methods:
14 *
15 * [string status, double newLast]
16 * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
17 * [optional] double aSpeed, [optional] double aLastSec)
18 *
19 * string progress
20 * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
21 *
22 * [string timeLeft, double newLast]
23 * getTimeLeft(double aSeconds, [optional] double aLastSec)
24 *
25 * [string dateCompact, string dateComplete]
26 * getReadableDates(Date aDate, [optional] Date aNow)
27 *
28 * [string displayHost, string fullHost]
29 * getURIHost(string aURIString)
30 *
31 * [string convertedBytes, string units]
32 * convertByteUnits(int aBytes)
33 *
34 * [int time, string units, int subTime, string subUnits]
35 * convertTimeUnits(double aSecs)
36 */
38 const Cc = Components.classes;
39 const Ci = Components.interfaces;
40 const Cu = Components.utils;
42 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
44 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
45 "resource://gre/modules/PluralForm.jsm");
47 this.__defineGetter__("gDecimalSymbol", function() {
48 delete this.gDecimalSymbol;
49 return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
50 });
52 const kDownloadProperties =
53 "chrome://mozapps/locale/downloads/downloads.properties";
55 let gStr = {
56 statusFormat: "statusFormat3",
57 statusFormatInfiniteRate: "statusFormatInfiniteRate",
58 statusFormatNoRate: "statusFormatNoRate",
59 transferSameUnits: "transferSameUnits2",
60 transferDiffUnits: "transferDiffUnits2",
61 transferNoTotal: "transferNoTotal2",
62 timePair: "timePair2",
63 timeLeftSingle: "timeLeftSingle2",
64 timeLeftDouble: "timeLeftDouble2",
65 timeFewSeconds: "timeFewSeconds",
66 timeUnknown: "timeUnknown",
67 monthDate: "monthDate2",
68 yesterday: "yesterday",
69 doneScheme: "doneScheme2",
70 doneFileScheme: "doneFileScheme",
71 units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
72 // Update timeSize in convertTimeUnits if changing the length of this array
73 timeUnits: ["seconds", "minutes", "hours", "days"],
74 infiniteRate: "infiniteRate",
75 };
77 // This lazily initializes the string bundle upon first use.
78 this.__defineGetter__("gBundle", function() {
79 delete gBundle;
80 return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"].
81 getService(Ci.nsIStringBundleService).
82 createBundle(kDownloadProperties);
83 });
85 // Keep track of at most this many second/lastSec pairs so that multiple calls
86 // to getTimeLeft produce the same time left
87 const kCachedLastMaxSize = 10;
88 let gCachedLast = [];
90 this.DownloadUtils = {
91 /**
92 * Generate a full status string for a download given its current progress,
93 * total size, speed, last time remaining
94 *
95 * @param aCurrBytes
96 * Number of bytes transferred so far
97 * @param [optional] aMaxBytes
98 * Total number of bytes or -1 for unknown
99 * @param [optional] aSpeed
100 * Current transfer rate in bytes/sec or -1 for unknown
101 * @param [optional] aLastSec
102 * Last time remaining in seconds or Infinity for unknown
103 * @return A pair: [download status text, new value of "last seconds"]
104 */
105 getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
106 aSpeed, aLastSec)
107 {
108 let [transfer, timeLeft, newLast, normalizedSpeed]
109 = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
111 let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed);
113 let status;
114 if (rate === "Infinity") {
115 // Infinity download speed doesn't make sense. Show a localized phrase instead.
116 let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft];
117 status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params,
118 params.length);
119 }
120 else {
121 let params = [transfer, rate, unit, timeLeft];
122 status = gBundle.formatStringFromName(gStr.statusFormat, params,
123 params.length);
124 }
125 return [status, newLast];
126 },
128 /**
129 * Generate a status string for a download given its current progress,
130 * total size, speed, last time remaining. The status string contains the
131 * time remaining, as well as the total bytes downloaded. Unlike
132 * getDownloadStatus, it does not include the rate of download.
133 *
134 * @param aCurrBytes
135 * Number of bytes transferred so far
136 * @param [optional] aMaxBytes
137 * Total number of bytes or -1 for unknown
138 * @param [optional] aSpeed
139 * Current transfer rate in bytes/sec or -1 for unknown
140 * @param [optional] aLastSec
141 * Last time remaining in seconds or Infinity for unknown
142 * @return A pair: [download status text, new value of "last seconds"]
143 */
144 getDownloadStatusNoRate:
145 function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed,
146 aLastSec)
147 {
148 let [transfer, timeLeft, newLast]
149 = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
151 let params = [transfer, timeLeft];
152 let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params,
153 params.length);
154 return [status, newLast];
155 },
157 /**
158 * Helper function that returns a transfer string, a time remaining string,
159 * and a new value of "last seconds".
160 * @param aCurrBytes
161 * Number of bytes transferred so far
162 * @param [optional] aMaxBytes
163 * Total number of bytes or -1 for unknown
164 * @param [optional] aSpeed
165 * Current transfer rate in bytes/sec or -1 for unknown
166 * @param [optional] aLastSec
167 * Last time remaining in seconds or Infinity for unknown
168 * @return A triple: [amount transferred string, time remaining string,
169 * new value of "last seconds"]
170 */
171 _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes,
172 aMaxBytes, aSpeed,
173 aLastSec)
174 {
175 if (aMaxBytes == null)
176 aMaxBytes = -1;
177 if (aSpeed == null)
178 aSpeed = -1;
179 if (aLastSec == null)
180 aLastSec = Infinity;
182 // Calculate the time remaining if we have valid values
183 let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
184 (aMaxBytes - aCurrBytes) / aSpeed : -1;
186 let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes);
187 let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec);
188 return [transfer, timeLeft, newLast, aSpeed];
189 },
191 /**
192 * Generate the transfer progress string to show the current and total byte
193 * size. Byte units will be as large as possible and the same units for
194 * current and max will be suppressed for the former.
195 *
196 * @param aCurrBytes
197 * Number of bytes transferred so far
198 * @param [optional] aMaxBytes
199 * Total number of bytes or -1 for unknown
200 * @return The transfer progress text
201 */
202 getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
203 {
204 if (aMaxBytes == null)
205 aMaxBytes = -1;
207 let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
208 let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
210 // Figure out which byte progress string to display
211 let name, values;
212 if (aMaxBytes < 0) {
213 name = gStr.transferNoTotal;
214 values = [
215 progress,
216 progressUnits,
217 ];
218 } else if (progressUnits == totalUnits) {
219 name = gStr.transferSameUnits;
220 values = [
221 progress,
222 total,
223 totalUnits,
224 ];
225 } else {
226 name = gStr.transferDiffUnits;
227 values = [
228 progress,
229 progressUnits,
230 total,
231 totalUnits,
232 ];
233 }
235 return gBundle.formatStringFromName(name, values, values.length);
236 },
238 /**
239 * Generate a "time left" string given an estimate on the time left and the
240 * last time. The extra time is used to give a better estimate on the time to
241 * show. Both the time values are doubles instead of integers to help get
242 * sub-second accuracy for current and future estimates.
243 *
244 * @param aSeconds
245 * Current estimate on number of seconds left for the download
246 * @param [optional] aLastSec
247 * Last time remaining in seconds or Infinity for unknown
248 * @return A pair: [time left text, new value of "last seconds"]
249 */
250 getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
251 {
252 if (aLastSec == null)
253 aLastSec = Infinity;
255 if (aSeconds < 0)
256 return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec];
258 // Try to find a cached lastSec for the given second
259 aLastSec = gCachedLast.reduce(function(aResult, aItem)
260 aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
262 // Add the current second/lastSec pair unless we have too many
263 gCachedLast.push([aSeconds, aLastSec]);
264 if (gCachedLast.length > kCachedLastMaxSize)
265 gCachedLast.shift();
267 // Apply smoothing only if the new time isn't a huge change -- e.g., if the
268 // new time is more than half the previous time; this is useful for
269 // downloads that start/resume slowly
270 if (aSeconds > aLastSec / 2) {
271 // Apply hysteresis to favor downward over upward swings
272 // 30% of down and 10% of up (exponential smoothing)
273 let (diff = aSeconds - aLastSec) {
274 aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
275 }
277 // If the new time is similar, reuse something close to the last seconds,
278 // but subtract a little to provide forward progress
279 let diff = aSeconds - aLastSec;
280 let diffPct = diff / aLastSec * 100;
281 if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
282 aSeconds = aLastSec - (diff < 0 ? .4 : .2);
283 }
285 // Decide what text to show for the time
286 let timeLeft;
287 if (aSeconds < 4) {
288 // Be friendly in the last few seconds
289 timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds);
290 } else {
291 // Convert the seconds into its two largest units to display
292 let [time1, unit1, time2, unit2] =
293 DownloadUtils.convertTimeUnits(aSeconds);
295 let pair1 =
296 gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2);
297 let pair2 =
298 gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2);
300 // Only show minutes for under 1 hour unless there's a few minutes left;
301 // or the second pair is 0.
302 if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
303 timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle,
304 [pair1], 1);
305 } else {
306 // We've got 2 pairs of times to display
307 timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble,
308 [pair1, pair2], 2);
309 }
310 }
312 return [timeLeft, aSeconds];
313 },
315 /**
316 * Converts a Date object to two readable formats, one compact, one complete.
317 * The compact format is relative to the current date, and is not an accurate
318 * representation. For example, only the time is displayed for today. The
319 * complete format always includes both the date and the time, excluding the
320 * seconds, and is often shown when hovering the cursor over the compact
321 * representation.
322 *
323 * @param aDate
324 * Date object representing the date and time to format. It is assumed
325 * that this value represents a past date.
326 * @param [optional] aNow
327 * Date object representing the current date and time. The real date
328 * and time of invocation is used if this parameter is omitted.
329 * @return A pair: [compact text, complete text]
330 */
331 getReadableDates: function DU_getReadableDates(aDate, aNow)
332 {
333 if (!aNow) {
334 aNow = new Date();
335 }
337 let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"]
338 .getService(Ci.nsIScriptableDateFormat);
340 // Figure out when today begins
341 let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate());
343 // Figure out if the time is from today, yesterday, this week, etc.
344 let dateTimeCompact;
345 if (aDate >= today) {
346 // After today started, show the time
347 dateTimeCompact = dts.FormatTime("",
348 dts.timeFormatNoSeconds,
349 aDate.getHours(),
350 aDate.getMinutes(),
351 0);
352 } else if (today - aDate < (24 * 60 * 60 * 1000)) {
353 // After yesterday started, show yesterday
354 dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday);
355 } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) {
356 // After last week started, show day of week
357 dateTimeCompact = aDate.toLocaleFormat("%A");
358 } else {
359 // Show month/day
360 let month = aDate.toLocaleFormat("%B");
361 // Remove leading 0 by converting the date string to a number
362 let date = Number(aDate.toLocaleFormat("%d"));
363 dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2);
364 }
366 let dateTimeFull = dts.FormatDateTime("",
367 dts.dateFormatLong,
368 dts.timeFormatNoSeconds,
369 aDate.getFullYear(),
370 aDate.getMonth() + 1,
371 aDate.getDate(),
372 aDate.getHours(),
373 aDate.getMinutes(),
374 0);
376 return [dateTimeCompact, dateTimeFull];
377 },
379 /**
380 * Get the appropriate display host string for a URI string depending on if
381 * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
382 *
383 * @param aURIString
384 * The URI string to try getting an eTLD + 1, etc.
385 * @return A pair: [display host for the URI string, full host name]
386 */
387 getURIHost: function DU_getURIHost(aURIString)
388 {
389 let ioService = Cc["@mozilla.org/network/io-service;1"].
390 getService(Ci.nsIIOService);
391 let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
392 getService(Ci.nsIEffectiveTLDService);
393 let idnService = Cc["@mozilla.org/network/idn-service;1"].
394 getService(Ci.nsIIDNService);
396 // Get a URI that knows about its components
397 let uri = ioService.newURI(aURIString, null, null);
399 // Get the inner-most uri for schemes like jar:
400 if (uri instanceof Ci.nsINestedURI)
401 uri = uri.innermostURI;
403 let fullHost;
404 try {
405 // Get the full host name; some special URIs fail (data: jar:)
406 fullHost = uri.host;
407 } catch (e) {
408 fullHost = "";
409 }
411 let displayHost;
412 try {
413 // This might fail if it's an IP address or doesn't have more than 1 part
414 let baseDomain = eTLDService.getBaseDomain(uri);
416 // Convert base domain for display; ignore the isAscii out param
417 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
418 } catch (e) {
419 // Default to the host name
420 displayHost = fullHost;
421 }
423 // Check if we need to show something else for the host
424 if (uri.scheme == "file") {
425 // Display special text for file protocol
426 displayHost = gBundle.GetStringFromName(gStr.doneFileScheme);
427 fullHost = displayHost;
428 } else if (displayHost.length == 0) {
429 // Got nothing; show the scheme (data: about: moz-icon:)
430 displayHost =
431 gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1);
432 fullHost = displayHost;
433 } else if (uri.port != -1) {
434 // Tack on the port if it's not the default port
435 let port = ":" + uri.port;
436 displayHost += port;
437 fullHost += port;
438 }
440 return [displayHost, fullHost];
441 },
443 /**
444 * Converts a number of bytes to the appropriate unit that results in an
445 * internationalized number that needs fewer than 4 digits.
446 *
447 * @param aBytes
448 * Number of bytes to convert
449 * @return A pair: [new value with 3 sig. figs., its unit]
450 */
451 convertByteUnits: function DU_convertByteUnits(aBytes)
452 {
453 let unitIndex = 0;
455 // Convert to next unit if it needs 4 digits (after rounding), but only if
456 // we know the name of the next unit
457 while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
458 aBytes /= 1024;
459 unitIndex++;
460 }
462 // Get rid of insignificant bits by truncating to 1 or 0 decimal points
463 // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
464 // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100
465 aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0);
467 if (gDecimalSymbol != ".")
468 aBytes = aBytes.replace(".", gDecimalSymbol);
469 return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])];
470 },
472 /**
473 * Converts a number of seconds to the two largest units. Time values are
474 * whole numbers, and units have the correct plural/singular form.
475 *
476 * @param aSecs
477 * Seconds to convert into the appropriate 2 units
478 * @return 4-item array [first value, its unit, second value, its unit]
479 */
480 convertTimeUnits: function DU_convertTimeUnits(aSecs)
481 {
482 // These are the maximum values for seconds, minutes, hours corresponding
483 // with gStr.timeUnits without the last item
484 let timeSize = [60, 60, 24];
486 let time = aSecs;
487 let scale = 1;
488 let unitIndex = 0;
490 // Keep converting to the next unit while we have units left and the
491 // current one isn't the largest unit possible
492 while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
493 time /= timeSize[unitIndex];
494 scale *= timeSize[unitIndex];
495 unitIndex++;
496 }
498 let value = convertTimeUnitsValue(time);
499 let units = convertTimeUnitsUnits(value, unitIndex);
501 let extra = aSecs - value * scale;
502 let nextIndex = unitIndex - 1;
504 // Convert the extra time to the next largest unit
505 for (let index = 0; index < nextIndex; index++)
506 extra /= timeSize[index];
508 let value2 = convertTimeUnitsValue(extra);
509 let units2 = convertTimeUnitsUnits(value2, nextIndex);
511 return [value, units, value2, units2];
512 },
513 };
515 /**
516 * Private helper for convertTimeUnits that gets the display value of a time
517 *
518 * @param aTime
519 * Time value for display
520 * @return An integer value for the time rounded down
521 */
522 function convertTimeUnitsValue(aTime)
523 {
524 return Math.floor(aTime);
525 }
527 /**
528 * Private helper for convertTimeUnits that gets the display units of a time
529 *
530 * @param aTime
531 * Time value for display
532 * @param aIndex
533 * Index into gStr.timeUnits for the appropriate unit
534 * @return The appropriate plural form of the unit for the time
535 */
536 function convertTimeUnitsUnits(aTime, aIndex)
537 {
538 // Negative index would be an invalid unit, so just give empty
539 if (aIndex < 0)
540 return "";
542 return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex]));
543 }
545 /**
546 * Private helper function to log errors to the error console and command line
547 *
548 * @param aMsg
549 * Error message to log or an array of strings to concat
550 */
551 function log(aMsg)
552 {
553 let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
554 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
555 logStringMessage(msg);
556 dump(msg + "\n");
557 }