Merge m-c to inbound.
|
@ -121,7 +121,7 @@ Object.defineProperty(ContentTabActor.prototype, "url", {
|
|||
configurable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(ContentTabActor.prototype, "contentWindow", {
|
||||
Object.defineProperty(ContentTabActor.prototype, "window", {
|
||||
get: function() {
|
||||
return this.browser;
|
||||
},
|
||||
|
|
|
@ -979,6 +979,7 @@ let RemoteDebugger = {
|
|||
if (!DebuggerServer.initialized) {
|
||||
// Ask for remote connections.
|
||||
DebuggerServer.init(this.prompt.bind(this));
|
||||
DebuggerServer.chromeWindowType = "navigator:browser";
|
||||
DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
|
||||
// Until we implement unix domain socket, we enable content actors
|
||||
// only on development devices
|
||||
|
|
|
@ -11,6 +11,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
|
|||
"resource://gre/modules/BookmarkJSONUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
|
||||
"resource://gre/modules/PlacesBackups.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
|
||||
"resource://gre/modules/DownloadUtils.jsm");
|
||||
|
||||
var PlacesOrganizer = {
|
||||
_places: null,
|
||||
|
@ -411,6 +413,21 @@ var PlacesOrganizer = {
|
|||
|
||||
// Populate menu with backups.
|
||||
for (let i = 0; i < backupFiles.length; i++) {
|
||||
let [size, unit] = DownloadUtils.convertByteUnits(backupFiles[i].fileSize);
|
||||
let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
|
||||
[size, unit]);
|
||||
let sizeInfo;
|
||||
let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
|
||||
if (bookmarkCount != null) {
|
||||
sizeInfo = " (" + sizeString + " - " +
|
||||
PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
|
||||
bookmarkCount,
|
||||
[bookmarkCount]) +
|
||||
")";
|
||||
} else {
|
||||
sizeInfo = " (" + sizeString + ")";
|
||||
}
|
||||
|
||||
let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
|
||||
let m = restorePopup.insertBefore(document.createElement("menuitem"),
|
||||
document.getElementById("restoreFromFile"));
|
||||
|
@ -419,7 +436,8 @@ var PlacesOrganizer = {
|
|||
Ci.nsIScriptableDateFormat.dateFormatLong,
|
||||
backupDate.getFullYear(),
|
||||
backupDate.getMonth() + 1,
|
||||
backupDate.getDate()));
|
||||
backupDate.getDate()) +
|
||||
sizeInfo);
|
||||
m.setAttribute("value", backupFiles[i].leafName);
|
||||
m.setAttribute("oncommand",
|
||||
"PlacesOrganizer.onRestoreMenuItemClick(this);");
|
||||
|
@ -516,7 +534,7 @@ var PlacesOrganizer = {
|
|||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
let fpCallback = function fpCallback_done(aResult) {
|
||||
if (aResult != Ci.nsIFilePicker.returnCancel) {
|
||||
BookmarkJSONUtils.exportToFile(fp.file);
|
||||
PlacesBackups.saveBookmarksToJSONFile(fp.file);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ function run_test() {
|
|||
create_bookmarks_html("bookmarks.glue.html");
|
||||
|
||||
// Create our JSON backup copying bookmarks.glue.json to the profile folder.
|
||||
remove_all_JSON_backups();
|
||||
create_JSON_backup("bookmarks.glue.json");
|
||||
|
||||
// Remove current database file.
|
||||
|
|
|
@ -275,6 +275,7 @@ function run_test()
|
|||
{
|
||||
// Create our bookmarks.html from bookmarks.glue.html.
|
||||
create_bookmarks_html("bookmarks.glue.html");
|
||||
remove_all_JSON_backups();
|
||||
// Create our JSON backup from bookmarks.glue.json.
|
||||
create_JSON_backup("bookmarks.glue.json");
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ function run_test() {
|
|||
|
||||
// Create our JSON backup copying bookmarks.glue.json to the profile
|
||||
// folder. It will be ignored.
|
||||
remove_all_JSON_backups();
|
||||
create_JSON_backup("bookmarks.glue.json");
|
||||
|
||||
// Remove current database file.
|
||||
|
|
|
@ -29,6 +29,8 @@ let tests = [];
|
|||
tests.push({
|
||||
description: "Export to bookmarks.html if autoExportHTML is true.",
|
||||
exec: function() {
|
||||
remove_all_JSON_backups();
|
||||
|
||||
// Sanity check: we should have bookmarks on the toolbar.
|
||||
do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
|
||||
|
||||
|
@ -43,7 +45,7 @@ tests.push({
|
|||
// Check bookmarks.html has been created.
|
||||
check_bookmarks_html();
|
||||
// Check JSON backup has been created.
|
||||
check_JSON_backup();
|
||||
check_JSON_backup(true);
|
||||
|
||||
// Check preferences have not been reverted.
|
||||
do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML));
|
||||
|
@ -128,16 +130,10 @@ tests.push({
|
|||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function finish_test() {
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
var testIndex = 0;
|
||||
function next_test() {
|
||||
// Remove bookmarks.html from profile.
|
||||
remove_bookmarks_html();
|
||||
// Remove JSON backups from profile.
|
||||
remove_all_JSON_backups();
|
||||
|
||||
// Execute next test.
|
||||
let test = tests.shift();
|
||||
|
|
|
@ -19,8 +19,8 @@ const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
|
|||
|
||||
let EXPECTED_NOTIFICATIONS = [
|
||||
"places-shutdown"
|
||||
, "places-will-close-connection"
|
||||
, "places-expiration-finished"
|
||||
, "places-will-close-connection"
|
||||
, "places-connection-closed"
|
||||
];
|
||||
|
||||
|
|
|
@ -757,7 +757,7 @@ FilterView.prototype = {
|
|||
this._tokenOperatorLabel.setAttribute("value",
|
||||
L10N.getFormatStr("searchPanelToken", this._tokenSearchKey));
|
||||
this._lineOperatorLabel.setAttribute("value",
|
||||
L10N.getFormatStr("searchPanelLine", this._lineSearchKey));
|
||||
L10N.getFormatStr("searchPanelGoToLine", this._lineSearchKey));
|
||||
this._variableOperatorLabel.setAttribute("value",
|
||||
L10N.getFormatStr("searchPanelVariable", this._variableSearchKey));
|
||||
|
||||
|
|
|
@ -122,8 +122,8 @@
|
|||
key="tokenSearchKey"
|
||||
command="tokenSearchCommand"/>
|
||||
<menuitem id="se-dbg-cMenu-findLine"
|
||||
label="&debuggerUI.searchLine;"
|
||||
accesskey="&debuggerUI.searchLine.key;"
|
||||
label="&debuggerUI.searchGoToLine;"
|
||||
accesskey="&debuggerUI.searchGoToLine.key;"
|
||||
key="lineSearchKey"
|
||||
command="lineSearchCommand"/>
|
||||
<menuseparator/>
|
||||
|
@ -226,7 +226,7 @@
|
|||
modifiers="accel"
|
||||
command="tokenSearchCommand"/>
|
||||
<key id="lineSearchKey"
|
||||
key="&debuggerUI.searchLine.key;"
|
||||
key="&debuggerUI.searchGoToLine.key;"
|
||||
modifiers="accel"
|
||||
command="lineSearchCommand"/>
|
||||
<key id="variableSearchKey"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
@ -235,7 +235,8 @@ ResponsiveUI.prototype = {
|
|||
// Removed elements.
|
||||
this.container.removeChild(this.toolbar);
|
||||
this.stack.removeChild(this.resizer);
|
||||
this.stack.removeChild(this.resizeBar);
|
||||
this.stack.removeChild(this.resizeBarV);
|
||||
this.stack.removeChild(this.resizeBarH);
|
||||
|
||||
// Unset the responsive mode.
|
||||
this.container.removeAttribute("responsivemode");
|
||||
|
@ -305,7 +306,8 @@ ResponsiveUI.prototype = {
|
|||
* <stack class="browserStack"> From tabbrowser.xml
|
||||
* <browser/>
|
||||
* <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
|
||||
* <box class="devtools-responsiveui-resizebar" top="0" right="0"/>
|
||||
* <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
|
||||
* <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
|
||||
* </stack>
|
||||
* </vbox>
|
||||
*/
|
||||
|
@ -360,15 +362,22 @@ ResponsiveUI.prototype = {
|
|||
this.resizer.setAttribute("bottom", "0");
|
||||
this.resizer.onmousedown = this.bound_startResizing;
|
||||
|
||||
this.resizeBar = this.chromeDoc.createElement("box");
|
||||
this.resizeBar.className = "devtools-responsiveui-resizebar";
|
||||
this.resizeBar.setAttribute("top", "0");
|
||||
this.resizeBar.setAttribute("right", "0");
|
||||
this.resizeBar.onmousedown = this.bound_startResizing;
|
||||
this.resizeBarV = this.chromeDoc.createElement("box");
|
||||
this.resizeBarV.className = "devtools-responsiveui-resizebarV";
|
||||
this.resizeBarV.setAttribute("top", "0");
|
||||
this.resizeBarV.setAttribute("right", "0");
|
||||
this.resizeBarV.onmousedown = this.bound_startResizing;
|
||||
|
||||
this.resizeBarH = this.chromeDoc.createElement("box");
|
||||
this.resizeBarH.className = "devtools-responsiveui-resizebarH";
|
||||
this.resizeBarH.setAttribute("bottom", "0");
|
||||
this.resizeBarH.setAttribute("left", "0");
|
||||
this.resizeBarH.onmousedown = this.bound_startResizing;
|
||||
|
||||
this.container.insertBefore(this.toolbar, this.stack);
|
||||
this.stack.appendChild(this.resizer);
|
||||
this.stack.appendChild(this.resizeBar);
|
||||
this.stack.appendChild(this.resizeBarV);
|
||||
this.stack.appendChild(this.resizeBarH);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -567,7 +576,7 @@ ResponsiveUI.prototype = {
|
|||
*/
|
||||
setSize: function RUI_setSize(aWidth, aHeight) {
|
||||
aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
|
||||
aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_WIDTH);
|
||||
aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
|
||||
|
||||
// We resize the containing stack.
|
||||
let style = "max-width: %width;" +
|
||||
|
@ -581,7 +590,9 @@ ResponsiveUI.prototype = {
|
|||
this.stack.setAttribute("style", style);
|
||||
|
||||
if (!this.ignoreY)
|
||||
this.resizeBar.setAttribute("top", Math.round(aHeight / 2));
|
||||
this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
|
||||
if (!this.ignoreX)
|
||||
this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
|
||||
|
||||
let selectedPreset = this.menuitems.get(this.selectedItem);
|
||||
|
||||
|
@ -622,7 +633,8 @@ ResponsiveUI.prototype = {
|
|||
this.lastScreenX = aEvent.screenX;
|
||||
this.lastScreenY = aEvent.screenY;
|
||||
|
||||
this.ignoreY = (aEvent.target === this.resizeBar);
|
||||
this.ignoreY = (aEvent.target === this.resizeBarV);
|
||||
this.ignoreX = (aEvent.target === this.resizeBarH);
|
||||
|
||||
this.isResizing = true;
|
||||
},
|
||||
|
@ -638,6 +650,8 @@ ResponsiveUI.prototype = {
|
|||
|
||||
if (this.ignoreY)
|
||||
deltaY = 0;
|
||||
if (this.ignoreX)
|
||||
deltaX = 0;
|
||||
|
||||
let width = this.customPreset.width + deltaX;
|
||||
let height = this.customPreset.height + deltaY;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* ObservableObject
|
||||
*
|
||||
* An observable object is a JSON-like object that throws
|
||||
* events when its direct properties or properties of any
|
||||
* contained objects, are getting accessed or set.
|
||||
*
|
||||
* Inherits from EventEmitter.
|
||||
*
|
||||
* Properties:
|
||||
* ⬩ object: JSON-like object
|
||||
*
|
||||
* Events:
|
||||
* ⬩ "get" / path (array of property names)
|
||||
* ⬩ "set" / path / new value
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* let emitter = new ObservableObject({ x: { y: [10] } });
|
||||
* emitter.on("set", console.log);
|
||||
* emitter.on("get", console.log);
|
||||
* let obj = emitter.object;
|
||||
* obj.x.y[0] = 50;
|
||||
*
|
||||
*/
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
function ObservableObject(object = {}) {
|
||||
let handler = new Handler(this);
|
||||
this.object = new Proxy(object, handler);
|
||||
handler._wrappers.set(this.object, object);
|
||||
handler._paths.set(object, []);
|
||||
}
|
||||
|
||||
exports.ObservableObject = ObservableObject;
|
||||
|
||||
ObservableObject.prototype = new EventEmitter();
|
||||
|
||||
function isObject(value) {
|
||||
return Object(value) === value;
|
||||
}
|
||||
|
||||
function Handler(emitter) {
|
||||
this._emitter = emitter;
|
||||
this._wrappers = new WeakMap();
|
||||
this._paths = new WeakMap();
|
||||
}
|
||||
|
||||
Handler.prototype = {
|
||||
wrap: function(target, key, value) {
|
||||
let path;
|
||||
if (!isObject(value)) {
|
||||
path = this._paths.get(target).concat(key);
|
||||
} else if (this._wrappers.has(value)) {
|
||||
path = this._paths.get(value);
|
||||
} else {
|
||||
path = this._paths.get(target).concat(key);
|
||||
this._paths.set(value, path);
|
||||
let wrapper = new Proxy(value, this);
|
||||
this._wrappers.set(wrapper, value);
|
||||
value = wrapper;
|
||||
}
|
||||
return [value, path];
|
||||
},
|
||||
unwrap: function(target, key, value) {
|
||||
if (!isObject(value) || !this._wrappers.has(value)) {
|
||||
return [value, this._paths.get(target).concat(key)];
|
||||
}
|
||||
return [this._wrappers.get(value), this._paths.get(value)];
|
||||
},
|
||||
get: function(target, key) {
|
||||
let value = target[key];
|
||||
let [wrapped, path] = this.wrap(target, key, value);
|
||||
this._emitter.emit("get", path, value);
|
||||
return wrapped;
|
||||
},
|
||||
set: function(target, key, value) {
|
||||
let [wrapped, path] = this.unwrap(target, key, value);
|
||||
target[key] = value;
|
||||
this._emitter.emit("set", path, value);
|
||||
},
|
||||
getOwnPropertyDescriptor: function(target, key) {
|
||||
let desc = Object.getOwnPropertyDescriptor(target, key);
|
||||
if (desc) {
|
||||
if ("value" in desc) {
|
||||
let [wrapped, path] = this.wrap(target, key, desc.value);
|
||||
desc.value = wrapped
|
||||
this._emitter.emit("get", path, desc.value);
|
||||
} else {
|
||||
if ("get" in desc) {
|
||||
[desc.get] = this.wrap(target, "get "+key, desc.get);
|
||||
}
|
||||
if ("set" in desc) {
|
||||
[desc.set] = this.wrap(target, "set "+key, desc.set);
|
||||
}
|
||||
}
|
||||
}
|
||||
return desc;
|
||||
},
|
||||
defineProperty: function(target, key, desc) {
|
||||
if ("value" in desc) {
|
||||
[desc.value, path] = this.unwrap(target, key, desc.value);
|
||||
Object.defineProperty(target, key, desc);
|
||||
this._emitter.emit("set", path, desc.value);
|
||||
} else {
|
||||
if ("get" in desc) {
|
||||
[desc.get] = this.unwrap(target, "get "+key, desc.get);
|
||||
}
|
||||
if ("set" in desc) {
|
||||
[desc.set] = this.unwrap(target, "set "+key, desc.set);
|
||||
}
|
||||
Object.defineProperty(target, key, desc);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -13,6 +13,7 @@ include $(DEPTH)/config/autoconf.mk
|
|||
|
||||
MOCHITEST_BROWSER_FILES = \
|
||||
browser_eventemitter_basic.js \
|
||||
browser_observableobject.js \
|
||||
browser_layoutHelpers.js \
|
||||
browser_require_basic.js \
|
||||
browser_telemetry_buttonsandsidebar.js \
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
let tmp = {};
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm", tmp);
|
||||
let {ObservableObject} = tmp.devtools.require("devtools/shared/observable-object");
|
||||
|
||||
let rawObject = {};
|
||||
let oe = new ObservableObject(rawObject);
|
||||
|
||||
function str(o) {
|
||||
return JSON.stringify(o);
|
||||
}
|
||||
|
||||
function areObjectsSynced() {
|
||||
is(str(rawObject), str(oe.object), "Objects are synced");
|
||||
}
|
||||
|
||||
areObjectsSynced();
|
||||
|
||||
let index = 0;
|
||||
let expected = [
|
||||
{type: "set", path: "foo", value: 4},
|
||||
{type: "get", path: "foo", value: 4},
|
||||
{type: "get", path: "foo", value: 4},
|
||||
{type: "get", path: "bar", value: undefined},
|
||||
{type: "get", path: "bar", value: undefined},
|
||||
{type: "set", path: "bar", value: {}},
|
||||
{type: "get", path: "bar", value: {}},
|
||||
{type: "get", path: "bar", value: {}},
|
||||
{type: "set", path: "bar.a", value: [1,2,3,4]},
|
||||
{type: "get", path: "bar", value: {a:[1,2,3,4]}},
|
||||
{type: "set", path: "bar.mop", value: 1},
|
||||
{type: "set", path: "bar", value: {}},
|
||||
{type: "set", path: "foo", value: [{a:42}]},
|
||||
{type: "get", path: "foo", value: [{a:42}]},
|
||||
{type: "get", path: "foo.0", value: {a:42}},
|
||||
{type: "get", path: "foo.0.a", value: 42},
|
||||
{type: "get", path: "foo", value: [{a:42}]},
|
||||
{type: "get", path: "foo.0", value: {a:42}},
|
||||
{type: "set", path: "foo.0.a", value: 2},
|
||||
];
|
||||
|
||||
function callback(event, path, value) {
|
||||
oe.off("get", callback);
|
||||
let e = expected[index];
|
||||
is(event, e.type, "[" + index + "] Right event received");
|
||||
is(path.join("."), e.path, "[" + index + "] Path valid");
|
||||
is(str(value), str(e.value), "[" + index + "] Value valid");
|
||||
index++;
|
||||
areObjectsSynced();
|
||||
oe.on("get", callback);
|
||||
if (index == expected.length) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
oe.on("set", callback);
|
||||
oe.on("get", callback);
|
||||
|
||||
oe.object.foo = 4;
|
||||
oe.object.foo;
|
||||
Object.getOwnPropertyDescriptor(oe.object, "foo")
|
||||
oe.object["bar"];
|
||||
oe.object.bar;
|
||||
oe.object.bar = {};
|
||||
oe.object.bar;
|
||||
oe.object.bar.a = [1,2,3,4];
|
||||
Object.defineProperty(oe.object.bar, "mop", {value:1});
|
||||
oe.object.bar = {};
|
||||
oe.object.foo = [{a:42}];
|
||||
oe.object.foo[0].a;
|
||||
oe.object.foo[0].a = 2;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
|
|
@ -88,8 +88,8 @@
|
|||
|
||||
<!-- LOCALIZATION NOTE (debuggerUI.searchLine): This is the text that appears
|
||||
- in the source editor's context menu for the line search operation. -->
|
||||
<!ENTITY debuggerUI.searchLine "Jump to line…">
|
||||
<!ENTITY debuggerUI.searchLine.key "J">
|
||||
<!ENTITY debuggerUI.searchGoToLine "Go to line…">
|
||||
<!ENTITY debuggerUI.searchGoToLine.key "L">
|
||||
|
||||
<!-- LOCALIZATION NOTE (debuggerUI.searchVariable): This is the text that appears
|
||||
- in the source editor's context menu for the variables search operation. -->
|
||||
|
|
|
@ -107,9 +107,9 @@ searchPanelFunction=Search for function definition (%S)
|
|||
# filter panel popup for the token search operation.
|
||||
searchPanelToken=Find in this file (%S)
|
||||
|
||||
# LOCALIZATION NOTE (searchPanelLine): This is the text that appears in the
|
||||
# LOCALIZATION NOTE (searchPanelGoToLine): This is the text that appears in the
|
||||
# filter panel popup for the line search operation.
|
||||
searchPanelLine=Jump to line (%S)
|
||||
searchPanelGoToLine=Go to line (%S)
|
||||
|
||||
# LOCALIZATION NOTE (searchPanelVariable): This is the text that appears in the
|
||||
# filter panel popup for the variables search operation.
|
||||
|
|
После Ширина: | Высота: | Размер: 256 B |
|
@ -195,6 +195,7 @@ browser.jar:
|
|||
skin/classic/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
|
||||
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
|
||||
skin/classic/browser/devtools/toggle-tools.png (devtools/toggle-tools.png)
|
||||
skin/classic/browser/devtools/dock-bottom.png (devtools/dock-bottom.png)
|
||||
|
|
После Ширина: | Высота: | Размер: 256 B |
|
@ -286,6 +286,7 @@ browser.jar:
|
|||
skin/classic/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
|
||||
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
|
||||
skin/classic/browser/devtools/toggle-tools.png (devtools/toggle-tools.png)
|
||||
skin/classic/browser/devtools/dock-bottom.png (devtools/dock-bottom.png)
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
.devtools-responsiveui-resizebar {
|
||||
.devtools-responsiveui-resizebarV {
|
||||
width: 7px;
|
||||
height: 24px;
|
||||
cursor: ew-resize;
|
||||
|
@ -42,6 +42,14 @@
|
|||
background-image: url("chrome://browser/skin/devtools/responsive-vertical-resizer.png");
|
||||
}
|
||||
|
||||
.devtools-responsiveui-resizebarH {
|
||||
width: 24px;
|
||||
height: 7px;
|
||||
cursor: ns-resize;
|
||||
transform: translate(12px, 12px);
|
||||
background-image: url("chrome://browser/skin/devtools/responsive-horizontal-resizer.png");
|
||||
}
|
||||
|
||||
.devtools-responsiveui-resizehandle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
|
После Ширина: | Высота: | Размер: 256 B |
|
@ -222,6 +222,7 @@ browser.jar:
|
|||
skin/classic/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
|
||||
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
|
||||
skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
|
||||
skin/classic/browser/devtools/toggle-tools.png (devtools/toggle-tools.png)
|
||||
skin/classic/browser/devtools/dock-bottom.png (devtools/dock-bottom.png)
|
||||
|
@ -478,6 +479,7 @@ browser.jar:
|
|||
skin/classic/aero/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
|
||||
skin/classic/aero/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
|
||||
skin/classic/aero/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
|
||||
skin/classic/aero/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
|
||||
skin/classic/aero/browser/devtools/responsive-background.png (devtools/responsive-background.png)
|
||||
skin/classic/aero/browser/devtools/toggle-tools.png (devtools/toggle-tools.png)
|
||||
skin/classic/aero/browser/devtools/dock-bottom.png (devtools/dock-bottom.png)
|
||||
|
|
|
@ -2477,7 +2477,7 @@ private:
|
|||
uint32_t mLength;
|
||||
};
|
||||
public:
|
||||
StringBuilder() : mLast(this), mLength(0)
|
||||
StringBuilder() : mLast(MOZ_THIS_IN_INITIALIZER_LIST()), mLength(0)
|
||||
{
|
||||
MOZ_COUNT_CTOR(StringBuilder);
|
||||
}
|
||||
|
|
|
@ -190,6 +190,32 @@ var Addons = {
|
|||
return outer;
|
||||
},
|
||||
|
||||
_createBrowseItem: function _createBrowseItem() {
|
||||
let outer = document.createElement("div");
|
||||
outer.className = "addon-item list-item";
|
||||
outer.setAttribute("role", "button");
|
||||
outer.addEventListener("click", function() {
|
||||
openLink(document.getElementById("header-button"));
|
||||
}.bind(this), true);
|
||||
|
||||
let img = document.createElement("img");
|
||||
img.className = "icon";
|
||||
img.setAttribute("src", "chrome://browser/skin/images/amo-logo.png");
|
||||
outer.appendChild(img);
|
||||
|
||||
let inner = document.createElement("div");
|
||||
inner.className = "inner";
|
||||
|
||||
let title = document.createElement("div");
|
||||
title.id = "browse-title";
|
||||
title.className = "title";
|
||||
title.textContent = gStringBundle.GetStringFromName("addons.browseAll");;
|
||||
inner.appendChild(title);
|
||||
|
||||
outer.appendChild(inner);
|
||||
return outer;
|
||||
},
|
||||
|
||||
_createItemForAddon: function _createItemForAddon(aAddon) {
|
||||
let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION);
|
||||
let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
|
||||
|
@ -269,6 +295,10 @@ var Addons = {
|
|||
item.addon = addon;
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
// Add a "Browse all Firefox Add-ons" item to the bottom of the list.
|
||||
let browseItem = self._createBrowseItem();
|
||||
list.appendChild(browseItem);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -10,3 +10,5 @@ addonType.locale=Locale
|
|||
addonType.search=Search
|
||||
|
||||
addonStatus.uninstalled=%S will be uninstalled after restart.
|
||||
|
||||
addons.browseAll=Browse all Firefox Add-ons
|
||||
|
|
|
@ -33,8 +33,18 @@
|
|||
bottom: -3px;
|
||||
}
|
||||
|
||||
#browse-title {
|
||||
margin-top: 1em;
|
||||
background-image: url("chrome://browser/skin/images/chevron.png");
|
||||
background-size: 8px 20px;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#header-button {
|
||||
background-image: url("chrome://browser/skin/images/addons-amo-hdpi.png");
|
||||
background-image: url("chrome://browser/skin/images/amo-logo.png"), url("chrome://browser/skin/images/chevron.png");
|
||||
background-size: 20px 20px, 8px 20px;
|
||||
background-position: left, right 3px center;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
#header-button {
|
||||
background-image: url("chrome://browser/skin/images/addons-amo-hdpi.png");
|
||||
background-image: url("chrome://browser/skin/images/marketplace-logo.png");
|
||||
}
|
||||
|
||||
#main-container {
|
||||
|
|
После Ширина: | Высота: | Размер: 653 B |
После Ширина: | Высота: | Размер: 354 B |
До Ширина: | Высота: | Размер: 4.0 KiB После Ширина: | Высота: | Размер: 4.0 KiB |
|
@ -33,6 +33,7 @@ chrome.jar:
|
|||
|
||||
skin/images/5stars.png (images/5stars.png)
|
||||
skin/images/addons-32.png (images/addons-32.png)
|
||||
skin/images/amo-logo.png (images/amo-logo.png)
|
||||
skin/images/arrowleft-16.png (images/arrowleft-16.png)
|
||||
skin/images/arrowright-16.png (images/arrowright-16.png)
|
||||
skin/images/arrowdown-16.png (images/arrowdown-16.png)
|
||||
|
@ -42,12 +43,14 @@ chrome.jar:
|
|||
skin/images/checkbox_unchecked.png (images/checkbox_unchecked.png)
|
||||
skin/images/checkbox_unchecked_disabled.png (images/checkbox_unchecked_disabled.png)
|
||||
skin/images/checkbox_unchecked_pressed.png (images/checkbox_unchecked_pressed.png)
|
||||
skin/images/chevron.png (images/chevron.png)
|
||||
skin/images/default-app-icon.png (images/default-app-icon.png)
|
||||
skin/images/dropmarker.svg (images/dropmarker.svg)
|
||||
skin/images/errorpage-warning.png (images/errorpage-warning.png)
|
||||
skin/images/errorpage-warning.png (images/errorpage-warning.png)
|
||||
skin/images/errorpage-larry-white.png (images/errorpage-larry-white.png)
|
||||
skin/images/errorpage-larry-black.png (images/errorpage-larry-black.png)
|
||||
skin/images/marketplace-logo.png (images/marketplace-logo.png)
|
||||
skin/images/throbber.png (images/throbber.png)
|
||||
skin/images/search-clear-30.png (images/search-clear-30.png)
|
||||
skin/images/play-hdpi.png (images/play-hdpi.png)
|
||||
|
@ -58,7 +61,6 @@ chrome.jar:
|
|||
skin/images/about-btn-darkgrey.png (images/about-btn-darkgrey.png)
|
||||
skin/images/logo-hdpi.png (images/logo-hdpi.png)
|
||||
skin/images/wordmark-hdpi.png (images/wordmark-hdpi.png)
|
||||
skin/images/addons-amo-hdpi.png (images/addons-amo-hdpi.png)
|
||||
skin/images/reader-plus-icon-mdpi.png (images/reader-plus-icon-mdpi.png)
|
||||
skin/images/reader-plus-icon-hdpi.png (images/reader-plus-icon-hdpi.png)
|
||||
skin/images/reader-plus-icon-xhdpi.png (images/reader-plus-icon-xhdpi.png)
|
||||
|
|
|
@ -183,6 +183,19 @@ Download.prototype = {
|
|||
*/
|
||||
currentBytes: 0,
|
||||
|
||||
/**
|
||||
* Indicates whether, at this time, there is any partially downloaded data
|
||||
* that can be used when restarting a failed or canceled download.
|
||||
*
|
||||
* This property is relevant while the download is in progress, and also if it
|
||||
* failed or has been canceled. If the download has been completed
|
||||
* successfully, this property is not relevant anymore.
|
||||
*
|
||||
* Whether partial data can actually be retained depends on the saver and the
|
||||
* download source, and may not be known before the download is started.
|
||||
*/
|
||||
hasPartialData: false,
|
||||
|
||||
/**
|
||||
* This can be set to a function that is called after other properties change.
|
||||
*/
|
||||
|
@ -266,6 +279,13 @@ Download.prototype = {
|
|||
return this._currentAttempt;
|
||||
}
|
||||
|
||||
// While shutting down or disposing of this object, we prevent the download
|
||||
// from returning to be in progress.
|
||||
if (this._finalized) {
|
||||
return Promise.reject(new DownloadError(Cr.NS_ERROR_FAILURE,
|
||||
"Cannot start after finalization."));
|
||||
}
|
||||
|
||||
// Initialize all the status properties for a new or restarted download.
|
||||
this.stopped = false;
|
||||
this.canceled = false;
|
||||
|
@ -284,10 +304,10 @@ Download.prototype = {
|
|||
|
||||
// This function propagates progress from the DownloadSaver object, unless
|
||||
// it comes in late from a download attempt that was replaced by a new one.
|
||||
function DS_setProgressBytes(aCurrentBytes, aTotalBytes)
|
||||
function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
|
||||
{
|
||||
if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
|
||||
this._setBytes(aCurrentBytes, aTotalBytes);
|
||||
this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -315,10 +335,18 @@ Download.prototype = {
|
|||
// Now that we stored the promise in the download object, we can start the
|
||||
// task that will actually execute the download.
|
||||
deferAttempt.resolve(Task.spawn(function task_D_start() {
|
||||
// Wait upon any pending cancellation request.
|
||||
// Wait upon any pending operation before restarting.
|
||||
if (this._promiseCanceled) {
|
||||
yield this._promiseCanceled;
|
||||
}
|
||||
if (this._promiseRemovePartialData) {
|
||||
try {
|
||||
yield this._promiseRemovePartialData;
|
||||
} catch (ex) {
|
||||
// Ignore any errors, which are already reported by the original
|
||||
// caller of the removePartialData method.
|
||||
}
|
||||
}
|
||||
|
||||
// Disallow download if parental controls service restricts it.
|
||||
if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
|
||||
|
@ -473,6 +501,83 @@ Download.prototype = {
|
|||
return this._promiseCanceled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether any partially downloaded data should be retained, to use
|
||||
* when restarting a failed or canceled download. The default is false.
|
||||
*
|
||||
* Whether partial data can actually be retained depends on the saver and the
|
||||
* download source, and may not be known before the download is started.
|
||||
*
|
||||
* To have any effect, this property must be set before starting the download.
|
||||
* Resetting this property to false after the download has already started
|
||||
* will not remove any partial data.
|
||||
*
|
||||
* If this property is set to true, care should be taken that partial data is
|
||||
* removed before the reference to the download is discarded. This can be
|
||||
* done using the removePartialData or the "finalize" methods.
|
||||
*/
|
||||
tryToKeepPartialData: false,
|
||||
|
||||
/**
|
||||
* When a request to remove partially downloaded data is received, contains a
|
||||
* promise that will be resolved when the removal request is processed. When
|
||||
* the request is processed, this property becomes null again.
|
||||
*/
|
||||
_promiseRemovePartialData: null,
|
||||
|
||||
/**
|
||||
* Removes any partial data kept as part of a canceled or failed download.
|
||||
*
|
||||
* If the download is not canceled or failed, this method has no effect, and
|
||||
* it returns a resolved promise. If the "cancel" method was called but the
|
||||
* cancellation process has not finished yet, this method waits for the
|
||||
* cancellation to finish, then removes the partial data.
|
||||
*
|
||||
* After this method has been called, if the tryToKeepPartialData property is
|
||||
* still true when the download is restarted, partial data will be retained
|
||||
* during the new download attempt.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the partial data has been successfully removed.
|
||||
* @rejects JavaScript exception if the operation could not be completed.
|
||||
*/
|
||||
removePartialData: function ()
|
||||
{
|
||||
if (!this.canceled && !this.error) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let promiseRemovePartialData = this._promiseRemovePartialData;
|
||||
|
||||
if (!promiseRemovePartialData) {
|
||||
let deferRemovePartialData = Promise.defer();
|
||||
promiseRemovePartialData = deferRemovePartialData.promise;
|
||||
this._promiseRemovePartialData = promiseRemovePartialData;
|
||||
|
||||
deferRemovePartialData.resolve(
|
||||
Task.spawn(function task_D_removePartialData() {
|
||||
try {
|
||||
// Wait upon any pending cancellation request.
|
||||
if (this._promiseCanceled) {
|
||||
yield this._promiseCanceled;
|
||||
}
|
||||
// Ask the saver object to remove any partial data.
|
||||
yield this.saver.removePartialData();
|
||||
// For completeness, clear the number of bytes transferred.
|
||||
if (this.currentBytes != 0 || this.hasPartialData) {
|
||||
this.currentBytes = 0;
|
||||
this.hasPartialData = false;
|
||||
this._notifyChange();
|
||||
}
|
||||
} finally {
|
||||
this._promiseRemovePartialData = null;
|
||||
}
|
||||
}.bind(this)));
|
||||
}
|
||||
|
||||
return promiseRemovePartialData;
|
||||
},
|
||||
|
||||
/**
|
||||
* This deferred object contains a promise that is resolved as soon as this
|
||||
* download finishes successfully, and is never rejected. This property is
|
||||
|
@ -498,6 +603,48 @@ Download.prototype = {
|
|||
return this._deferSucceeded.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the "finalize" method has been called. This prevents the download
|
||||
* from starting again after having been stopped.
|
||||
*/
|
||||
_finalized: false,
|
||||
|
||||
/**
|
||||
* Ensures that the download is stopped, and optionally removes any partial
|
||||
* data kept as part of a canceled or failed download. After this method has
|
||||
* been called, the download cannot be started again.
|
||||
*
|
||||
* This method should be used in place of "cancel" and removePartialData while
|
||||
* shutting down or disposing of the download object, to prevent other callers
|
||||
* from interfering with the operation. This is required because cancellation
|
||||
* and other operations are asynchronous.
|
||||
*
|
||||
* @param aRemovePartialData
|
||||
* Whether any partially downloaded data should be removed after the
|
||||
* download has been stopped.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation has finished successfully.
|
||||
* @rejects JavaScript exception if an error occurred while removing the
|
||||
* partially downloaded data.
|
||||
*/
|
||||
finalize: function (aRemovePartialData)
|
||||
{
|
||||
// Prevents the download from starting again after having been stopped.
|
||||
this._finalized = true;
|
||||
|
||||
if (aRemovePartialData) {
|
||||
// Cancel the download, in case it is currently in progress, then remove
|
||||
// any partially downloaded data. The removal operation waits for
|
||||
// cancellation to be completed before resolving the promise it returns.
|
||||
this.cancel();
|
||||
return this.removePartialData();
|
||||
} else {
|
||||
// Just cancel the download, in case it is currently in progress.
|
||||
return this.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates progress notifications based on the number of bytes transferred.
|
||||
*
|
||||
|
@ -505,9 +652,13 @@ Download.prototype = {
|
|||
* Number of bytes transferred until now.
|
||||
* @param aTotalBytes
|
||||
* Total number of bytes to be transferred, or -1 if unknown.
|
||||
* @param aHasPartialData
|
||||
* Indicates whether the partially downloaded data can be used when
|
||||
* restarting the download if it fails or is canceled.
|
||||
*/
|
||||
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes) {
|
||||
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
|
||||
this.currentBytes = aCurrentBytes;
|
||||
this.hasPartialData = aHasPartialData;
|
||||
if (aTotalBytes != -1) {
|
||||
this.hasProgress = true;
|
||||
this.totalBytes = aTotalBytes;
|
||||
|
@ -680,11 +831,13 @@ DownloadSource.prototype = {
|
|||
DownloadSource.fromSerializable = function (aSerializable) {
|
||||
let source = new DownloadSource();
|
||||
if (isString(aSerializable)) {
|
||||
source.url = aSerializable;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
source.url = aSerializable.toString();
|
||||
} else if (aSerializable instanceof Ci.nsIURI) {
|
||||
source.url = aSerializable.spec;
|
||||
} else {
|
||||
source.url = aSerializable.url;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
source.url = aSerializable.url.toString();
|
||||
if ("isPrivate" in aSerializable) {
|
||||
source.isPrivate = aSerializable.isPrivate;
|
||||
}
|
||||
|
@ -710,6 +863,13 @@ DownloadTarget.prototype = {
|
|||
*/
|
||||
path: null,
|
||||
|
||||
/**
|
||||
* String containing the path of the ".part" file containing the data
|
||||
* downloaded so far, or null to disable the use of a ".part" file to keep
|
||||
* partially downloaded data.
|
||||
*/
|
||||
partFilePath: null,
|
||||
|
||||
/**
|
||||
* Returns a static representation of the current object state.
|
||||
*
|
||||
|
@ -717,8 +877,13 @@ DownloadTarget.prototype = {
|
|||
*/
|
||||
toSerializable: function ()
|
||||
{
|
||||
// Simplify the representation since we don't have other details for now.
|
||||
return this.path;
|
||||
// Simplify the representation if we don't have other details.
|
||||
if (!this.partFilePath) {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
return { path: this.path,
|
||||
partFilePath: this.partFilePath };
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -738,14 +903,18 @@ DownloadTarget.prototype = {
|
|||
DownloadTarget.fromSerializable = function (aSerializable) {
|
||||
let target = new DownloadTarget();
|
||||
if (isString(aSerializable)) {
|
||||
target.path = aSerializable;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
target.path = aSerializable.toString();
|
||||
} else if (aSerializable instanceof Ci.nsIFile) {
|
||||
// Read the "path" property of nsIFile after checking the object type.
|
||||
target.path = aSerializable.path;
|
||||
} else {
|
||||
// Read the "path" property of the serializable DownloadTarget
|
||||
// representation.
|
||||
target.path = aSerializable.path;
|
||||
// representation, converting String objects to primitive strings.
|
||||
target.path = aSerializable.path.toString();
|
||||
if ("partFilePath" in aSerializable) {
|
||||
target.partFilePath = aSerializable.partFilePath;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
@ -787,6 +956,7 @@ function DownloadError(aResult, aMessage, aInferCause)
|
|||
this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
|
||||
this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
|
||||
}
|
||||
this.stack = new Error().stack;
|
||||
}
|
||||
|
||||
DownloadError.prototype = {
|
||||
|
@ -831,6 +1001,10 @@ function DownloadSaver() { }
|
|||
DownloadSaver.prototype = {
|
||||
/**
|
||||
* Download object for raising notifications and reading properties.
|
||||
*
|
||||
* If the tryToKeepPartialData property of the download object is false, the
|
||||
* saver should never try to keep partially downloaded data if the download
|
||||
* fails.
|
||||
*/
|
||||
download: null,
|
||||
|
||||
|
@ -839,9 +1013,11 @@ DownloadSaver.prototype = {
|
|||
*
|
||||
* @param aSetProgressBytesFn
|
||||
* This function may be called by the saver to report progress. It
|
||||
* takes two arguments: the first is the number of bytes transferred
|
||||
* takes three arguments: the first is the number of bytes transferred
|
||||
* until now, the second is the total number of bytes to be
|
||||
* transferred, or -1 if unknown.
|
||||
* transferred (or -1 if unknown), the third indicates whether the
|
||||
* partially downloaded data can be used when restarting the download
|
||||
* if it fails or is canceled.
|
||||
* @parem aSetPropertiesFn
|
||||
* This function may be called by the saver to report information
|
||||
* about new download properties discovered by the saver during the
|
||||
|
@ -866,6 +1042,22 @@ DownloadSaver.prototype = {
|
|||
throw new Error("Not implemented.");
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes any partial data kept as part of a canceled or failed download.
|
||||
*
|
||||
* This method is never called until the promise returned by "execute" is
|
||||
* either resolved or rejected, and the "execute" method is not called again
|
||||
* until the promise returned by this method is resolved or rejected.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation has finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
removePartialData: function DS_removePartialData()
|
||||
{
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a static representation of the current object state.
|
||||
*
|
||||
|
@ -920,109 +1112,213 @@ DownloadCopySaver.prototype = {
|
|||
*/
|
||||
_backgroundFileSaver: null,
|
||||
|
||||
/**
|
||||
* Indicates whether the "cancel" method has been called. This is used to
|
||||
* prevent the request from starting in case the operation is canceled before
|
||||
* the BackgroundFileSaver instance has been created.
|
||||
*/
|
||||
_canceled: false,
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.execute".
|
||||
*/
|
||||
execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let copySaver = this;
|
||||
|
||||
this._canceled = false;
|
||||
|
||||
let download = this.download;
|
||||
let targetPath = download.target.path;
|
||||
let partFilePath = download.target.partFilePath;
|
||||
let keepPartialData = download.tryToKeepPartialData;
|
||||
|
||||
// Create the object that will save the file in a background thread.
|
||||
let backgroundFileSaver = new BackgroundFileSaverStreamListener();
|
||||
try {
|
||||
// When the operation completes, reflect the status in the promise
|
||||
// returned by this download execution function.
|
||||
backgroundFileSaver.observer = {
|
||||
onTargetChange: function () { },
|
||||
onSaveComplete: function DCSE_onSaveComplete(aSaver, aStatus)
|
||||
{
|
||||
// Free the reference cycle, in order to release resources earlier.
|
||||
backgroundFileSaver.observer = null;
|
||||
this._backgroundFileSaver = null;
|
||||
|
||||
// Send notifications now that we can restart the download if needed.
|
||||
if (Components.isSuccessCode(aStatus)) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
// Infer the origin of the error from the failure code, because
|
||||
// BackgroundFileSaver does not provide more specific data.
|
||||
deferred.reject(new DownloadError(aStatus, null, true));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Set the target file, that will be deleted if the download fails.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(download.target.path),
|
||||
false);
|
||||
|
||||
// Create a channel from the source, and listen to progress notifications.
|
||||
let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
|
||||
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
channel.setPrivate(download.source.isPrivate);
|
||||
}
|
||||
if (channel instanceof Ci.nsIHttpChannel && download.source.referrer) {
|
||||
channel.referrer = NetUtil.newURI(download.source.referrer);
|
||||
return Task.spawn(function task_DCS_execute() {
|
||||
// To reduce the chance that other downloads reuse the same final target
|
||||
// file name, we should create a placeholder as soon as possible, before
|
||||
// starting the network request. The placeholder is also required in case
|
||||
// we are using a ".part" file instead of the final target while the
|
||||
// download is in progress.
|
||||
try {
|
||||
// If the file already exists, don't delete its contents yet.
|
||||
let file = yield OS.File.open(targetPath, { write: true });
|
||||
yield file.close();
|
||||
} catch (ex if ex instanceof OS.File.Error) {
|
||||
// Throw a DownloadError indicating that the operation failed because of
|
||||
// the target file. We cannot translate this into a specific result
|
||||
// code, but we preserve the original message using the toString method.
|
||||
let error = new DownloadError(Cr.NS_ERROR_FAILURE, ex.toString());
|
||||
error.becauseTargetFailed = true;
|
||||
throw error;
|
||||
}
|
||||
|
||||
channel.notificationCallbacks = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
|
||||
getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
|
||||
onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
|
||||
aProgressMax)
|
||||
{
|
||||
aSetProgressBytesFn(aProgress, aProgressMax);
|
||||
},
|
||||
onStatus: function () { },
|
||||
};
|
||||
try {
|
||||
let deferSaveComplete = Promise.defer();
|
||||
|
||||
// Open the channel, directing output to the background file saver.
|
||||
backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
|
||||
channel.asyncOpen({
|
||||
onStartRequest: function DCSE_onStartRequest(aRequest, aContext)
|
||||
{
|
||||
backgroundFileSaver.onStartRequest(aRequest, aContext);
|
||||
if (this._canceled) {
|
||||
// Don't create the BackgroundFileSaver object if we have been
|
||||
// canceled meanwhile.
|
||||
throw new DownloadError(Cr.NS_ERROR_FAILURE, "Saver canceled.");
|
||||
}
|
||||
|
||||
// Ensure we report the value of "Content-Length", if available, even
|
||||
// if the download doesn't generate any progress events later.
|
||||
if (aRequest instanceof Ci.nsIChannel &&
|
||||
aRequest.contentLength >= 0) {
|
||||
aSetProgressBytesFn(0, aRequest.contentLength);
|
||||
aSetPropertiesFn({ contentType: aRequest.contentType });
|
||||
// Create the object that will save the file in a background thread.
|
||||
let backgroundFileSaver = new BackgroundFileSaverStreamListener();
|
||||
try {
|
||||
// When the operation completes, reflect the status in the promise
|
||||
// returned by this download execution function.
|
||||
backgroundFileSaver.observer = {
|
||||
onTargetChange: function () { },
|
||||
onSaveComplete: function DCSE_onSaveComplete(aSaver, aStatus)
|
||||
{
|
||||
// Free the reference cycle, to release resources earlier.
|
||||
backgroundFileSaver.observer = null;
|
||||
this._backgroundFileSaver = null;
|
||||
|
||||
// Send notifications now that we can restart if needed.
|
||||
if (Components.isSuccessCode(aStatus)) {
|
||||
deferSaveComplete.resolve();
|
||||
} else {
|
||||
// Infer the origin of the error from the failure code, because
|
||||
// BackgroundFileSaver does not provide more specific data.
|
||||
deferSaveComplete.reject(new DownloadError(aStatus, null,
|
||||
true));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Create a channel from the source, and listen to progress
|
||||
// notifications.
|
||||
let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
|
||||
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
channel.setPrivate(download.source.isPrivate);
|
||||
}
|
||||
},
|
||||
onStopRequest: function DCSE_onStopRequest(aRequest, aContext,
|
||||
aStatusCode)
|
||||
{
|
||||
try {
|
||||
backgroundFileSaver.onStopRequest(aRequest, aContext, aStatusCode);
|
||||
} finally {
|
||||
// If the data transfer completed successfully, indicate to the
|
||||
// background file saver that the operation can finish. If the
|
||||
// data transfer failed, the saver has been already stopped.
|
||||
if (Components.isSuccessCode(aStatusCode)) {
|
||||
backgroundFileSaver.finish(Cr.NS_OK);
|
||||
}
|
||||
if (channel instanceof Ci.nsIHttpChannel &&
|
||||
download.source.referrer) {
|
||||
channel.referrer = NetUtil.newURI(download.source.referrer);
|
||||
}
|
||||
},
|
||||
onDataAvailable: function DCSE_onDataAvailable(aRequest, aContext,
|
||||
aInputStream, aOffset,
|
||||
aCount)
|
||||
{
|
||||
backgroundFileSaver.onDataAvailable(aRequest, aContext, aInputStream,
|
||||
aOffset, aCount);
|
||||
},
|
||||
}, null);
|
||||
|
||||
// If the operation succeeded, store the object to allow cancellation.
|
||||
this._backgroundFileSaver = backgroundFileSaver;
|
||||
} catch (ex) {
|
||||
// In case an error occurs while setting up the chain of objects for the
|
||||
// download, ensure that we release the resources of the background saver.
|
||||
deferred.reject(ex);
|
||||
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
||||
}
|
||||
return deferred.promise;
|
||||
// If we have data that we can use to resume the download from where
|
||||
// it stopped, try to use it.
|
||||
let resumeAttempted = false;
|
||||
if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
|
||||
partFilePath && keepPartialData) {
|
||||
try {
|
||||
let stat = yield OS.File.stat(partFilePath);
|
||||
channel.resumeAt(stat.size, this.entityID);
|
||||
resumeAttempted = true;
|
||||
} catch (ex if ex instanceof OS.File.Error &&
|
||||
ex.becauseNoSuchFile) { }
|
||||
}
|
||||
|
||||
channel.notificationCallbacks = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
|
||||
getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
|
||||
onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
|
||||
aProgressMax)
|
||||
{
|
||||
aSetProgressBytesFn(aProgress, aProgressMax, aProgress > 0 &&
|
||||
partFilePath && keepPartialData);
|
||||
},
|
||||
onStatus: function () { },
|
||||
};
|
||||
|
||||
// Open the channel, directing output to the background file saver.
|
||||
backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
|
||||
channel.asyncOpen({
|
||||
onStartRequest: function (aRequest, aContext) {
|
||||
backgroundFileSaver.onStartRequest(aRequest, aContext);
|
||||
|
||||
// Ensure we report the value of "Content-Length", if available,
|
||||
// even if the download doesn't generate any progress events
|
||||
// later.
|
||||
if (aRequest instanceof Ci.nsIChannel &&
|
||||
aRequest.contentLength >= 0) {
|
||||
aSetProgressBytesFn(0, aRequest.contentLength);
|
||||
aSetPropertiesFn({ contentType: aRequest.contentType });
|
||||
}
|
||||
|
||||
if (keepPartialData) {
|
||||
// If the source is not resumable, don't keep partial data even
|
||||
// if we were asked to try and do it.
|
||||
if (aRequest instanceof Ci.nsIResumableChannel) {
|
||||
try {
|
||||
// If reading the ID succeeds, the source is resumable.
|
||||
this.entityID = aRequest.entityID;
|
||||
} catch (ex if ex instanceof Components.Exception &&
|
||||
ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
|
||||
keepPartialData = false;
|
||||
}
|
||||
} else {
|
||||
keepPartialData = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (partFilePath) {
|
||||
// If we actually resumed a request, append to the partial data.
|
||||
if (resumeAttempted) {
|
||||
// TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
|
||||
backgroundFileSaver.enableAppend();
|
||||
}
|
||||
|
||||
// Use a part file, determining if we should keep it on failure.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
|
||||
keepPartialData);
|
||||
} else {
|
||||
// Set the final target file, and delete it on failure.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
|
||||
false);
|
||||
}
|
||||
}.bind(copySaver),
|
||||
|
||||
onStopRequest: function (aRequest, aContext, aStatusCode) {
|
||||
try {
|
||||
backgroundFileSaver.onStopRequest(aRequest, aContext,
|
||||
aStatusCode);
|
||||
} finally {
|
||||
// If the data transfer completed successfully, indicate to the
|
||||
// background file saver that the operation can finish. If the
|
||||
// data transfer failed, the saver has been already stopped.
|
||||
if (Components.isSuccessCode(aStatusCode)) {
|
||||
if (partFilePath) {
|
||||
// Move to the final target if we were using a part file.
|
||||
backgroundFileSaver.setTarget(
|
||||
new FileUtils.File(targetPath), false);
|
||||
}
|
||||
backgroundFileSaver.finish(Cr.NS_OK);
|
||||
}
|
||||
}
|
||||
}.bind(copySaver),
|
||||
|
||||
onDataAvailable: function (aRequest, aContext, aInputStream,
|
||||
aOffset, aCount) {
|
||||
backgroundFileSaver.onDataAvailable(aRequest, aContext,
|
||||
aInputStream, aOffset,
|
||||
aCount);
|
||||
}.bind(copySaver),
|
||||
}, null);
|
||||
|
||||
// If the operation succeeded, store the object to allow cancellation.
|
||||
this._backgroundFileSaver = backgroundFileSaver;
|
||||
} catch (ex) {
|
||||
// In case an error occurs while setting up the chain of objects for
|
||||
// the download, ensure that we release the resources of the saver.
|
||||
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// We will wait on this promise in case no error occurred while setting
|
||||
// up the chain of objects for the download.
|
||||
yield deferSaveComplete.promise;
|
||||
} catch (ex) {
|
||||
// Ensure we always remove the placeholder for the final target file on
|
||||
// failure, independently of which code path failed. In some cases, the
|
||||
// background file saver may have already removed the file.
|
||||
try {
|
||||
yield OS.File.remove(targetPath);
|
||||
} catch (e2 if e2 instanceof OS.File.Error && e2.becauseNoSuchFile) { }
|
||||
throw ex;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1030,12 +1326,27 @@ DownloadCopySaver.prototype = {
|
|||
*/
|
||||
cancel: function DCS_cancel()
|
||||
{
|
||||
this._canceled = true;
|
||||
if (this._backgroundFileSaver) {
|
||||
this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
||||
this._backgroundFileSaver = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.removePartialData".
|
||||
*/
|
||||
removePartialData: function ()
|
||||
{
|
||||
return Task.spawn(function task_DCS_removePartialData() {
|
||||
if (this.download.target.partFilePath) {
|
||||
try {
|
||||
yield OS.File.remove(this.download.target.partFilePath);
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { }
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.toSerializable".
|
||||
*/
|
||||
|
@ -1120,8 +1431,11 @@ DownloadLegacySaver.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
let hasPartFile = !!this.download.target.partFilePath;
|
||||
|
||||
this.progressWasNotified = true;
|
||||
this.setProgressBytesFn(aCurrentBytes, aTotalBytes);
|
||||
this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
|
||||
aCurrentBytes > 0 && hasPartFile);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1129,6 +1443,25 @@ DownloadLegacySaver.prototype = {
|
|||
*/
|
||||
progressWasNotified: false,
|
||||
|
||||
/**
|
||||
* Called by the nsITransfer implementation when the request has started.
|
||||
*
|
||||
* @param aRequest
|
||||
* nsIRequest associated to the status update.
|
||||
*/
|
||||
onTransferStarted: function (aRequest)
|
||||
{
|
||||
// Store the entity ID to use for resuming if required.
|
||||
if (this.download.tryToKeepPartialData &&
|
||||
aRequest instanceof Ci.nsIResumableChannel) {
|
||||
try {
|
||||
// If reading the ID succeeds, the source is resumable.
|
||||
this.entityID = aRequest.entityID;
|
||||
} catch (ex if ex instanceof Components.Exception &&
|
||||
ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by the nsITransfer implementation when the request has finished.
|
||||
*
|
||||
|
@ -1151,11 +1484,36 @@ DownloadLegacySaver.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When the first execution of the download finished, it can be restarted by
|
||||
* using a DownloadCopySaver object instead of the original legacy component
|
||||
* that executed the download.
|
||||
*/
|
||||
firstExecutionFinished: false,
|
||||
|
||||
/**
|
||||
* In case the download is restarted after the first execution finished, this
|
||||
* property contains a reference to the DownloadCopySaver that is executing
|
||||
* the new download attempt.
|
||||
*/
|
||||
copySaver: null,
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.execute".
|
||||
*/
|
||||
execute: function DLS_execute(aSetProgressBytesFn)
|
||||
{
|
||||
// Check if this is not the first execution of the download. The Download
|
||||
// object guarantees that this function is not re-entered during execution.
|
||||
if (this.firstExecutionFinished) {
|
||||
if (!this.copySaver) {
|
||||
this.copySaver = new DownloadCopySaver();
|
||||
this.copySaver.download = this.download;
|
||||
this.copySaver.entityID = this.entityID;
|
||||
}
|
||||
return this.copySaver.execute.apply(this.copySaver, arguments);
|
||||
}
|
||||
|
||||
this.setProgressBytesFn = aSetProgressBytesFn;
|
||||
|
||||
return Task.spawn(function task_DLS_execute() {
|
||||
|
@ -1184,6 +1542,8 @@ DownloadLegacySaver.prototype = {
|
|||
} finally {
|
||||
// We don't need the reference to the request anymore.
|
||||
this.request = null;
|
||||
// Allow the download to restart through a DownloadCopySaver.
|
||||
this.firstExecutionFinished = true;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
@ -1193,6 +1553,11 @@ DownloadLegacySaver.prototype = {
|
|||
*/
|
||||
cancel: function DLS_cancel()
|
||||
{
|
||||
// We may be using a DownloadCopySaver to handle resuming.
|
||||
if (this.copySaver) {
|
||||
return this.copySaver.cancel.apply(this.copySaver, arguments);
|
||||
}
|
||||
|
||||
// Synchronously cancel the operation as soon as the object is connected.
|
||||
this.deferCanceled.resolve();
|
||||
|
||||
|
@ -1202,6 +1567,29 @@ DownloadLegacySaver.prototype = {
|
|||
this.deferExecuted.reject(new DownloadError(Cr.NS_ERROR_FAILURE,
|
||||
"Download canceled."));
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.removePartialData".
|
||||
*/
|
||||
removePartialData: function ()
|
||||
{
|
||||
// DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
|
||||
// partially downloaded data, though this implementation isn't shared by
|
||||
// other saver types, thus it isn't found on their shared prototype.
|
||||
return DownloadCopySaver.prototype.removePartialData.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.toSerializable".
|
||||
*/
|
||||
toSerializable: function ()
|
||||
{
|
||||
// This object depends on legacy components that are created externally,
|
||||
// thus it cannot be rebuilt during deserialization. To support resuming
|
||||
// across different browser sessions, this object is transformed into a
|
||||
// DownloadCopySaver for the purpose of serialization.
|
||||
return "copy";
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -87,10 +87,17 @@ DownloadLegacyTransfer.prototype = {
|
|||
this._componentFailed = true;
|
||||
}
|
||||
|
||||
// Detect when the last file has been received, or the download failed.
|
||||
if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
|
||||
if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
|
||||
(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
|
||||
// Wait for the associated Download object to be available.
|
||||
// The main request has just started. Wait for the associated Download
|
||||
// object to be available before notifying.
|
||||
this._deferDownload.promise.then(function (aDownload) {
|
||||
aDownload.saver.onTransferStarted(aRequest);
|
||||
}).then(null, Cu.reportError);
|
||||
} else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
|
||||
(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
|
||||
// The last file has been received, or the download failed. Wait for the
|
||||
// associated Download object to be available before notifying.
|
||||
this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
|
||||
aDownload.saver.onTransferFinished(aRequest, aStatus);
|
||||
}).then(null, Cu.reportError);
|
||||
|
@ -175,7 +182,8 @@ DownloadLegacyTransfer.prototype = {
|
|||
// download system to initialize before the object is created.
|
||||
Downloads.createDownload({
|
||||
source: { url: aSource.spec, isPrivate: aIsPrivate },
|
||||
target: aTarget.QueryInterface(Ci.nsIFileURL).file,
|
||||
target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
|
||||
partFilePath: aTempFile && aTempFile.path },
|
||||
saver: "legacy",
|
||||
launchWhenSuccedded: launchWhenSuccedded,
|
||||
contentType: contentType,
|
||||
|
@ -189,6 +197,11 @@ DownloadLegacyTransfer.prototype = {
|
|||
}
|
||||
}).then(null, Cu.reportError);
|
||||
|
||||
// Legacy components keep partial data when they use a ".part" file.
|
||||
if (aTempFile) {
|
||||
aDownload.tryToKeepPartialData = true;
|
||||
}
|
||||
|
||||
// Start the download before allowing it to be controlled.
|
||||
aDownload.start().then(null, function () {
|
||||
// In case the operation failed, ensure we stop downloading data.
|
||||
|
|
|
@ -104,6 +104,11 @@ DownloadList.prototype = {
|
|||
* Removes a download from the list. If the download was already removed,
|
||||
* this method has no effect.
|
||||
*
|
||||
* This method does not change the state of the download, to allow adding it
|
||||
* to another list, or control it directly. If you want to dispose of the
|
||||
* download object, you should cancel it afterwards, and remove any partially
|
||||
* downloaded data if needed.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to remove.
|
||||
*/
|
||||
|
@ -208,7 +213,14 @@ DownloadList.prototype = {
|
|||
// operation hasn't completed yet so we don't check "stopped" here.
|
||||
if ((download.succeeded || download.canceled || download.error) &&
|
||||
aTestFn(download)) {
|
||||
// Remove the download first, so that the views don't get the change
|
||||
// notifications that may occur during finalization.
|
||||
this.remove(download);
|
||||
// Ensure that the download is stopped and no partial data is kept.
|
||||
// This works even if the download state has changed meanwhile. We
|
||||
// don't need to wait for the procedure to be complete before
|
||||
// processing the other downloads in the list.
|
||||
download.finalize(true);
|
||||
}
|
||||
}
|
||||
}.bind(this)).then(null, Cu.reportError);
|
||||
|
|
|
@ -90,9 +90,12 @@ this.Downloads = {
|
|||
/**
|
||||
* Downloads data from a remote network location to a local file.
|
||||
*
|
||||
* This download method does not provide user interface or the ability to
|
||||
* cancel the download programmatically. For that, you should obtain a
|
||||
* reference to a Download object using the createDownload function.
|
||||
* This download method does not provide user interface, or the ability to
|
||||
* cancel or restart the download programmatically. For that, you should
|
||||
* obtain a reference to a Download object using the createDownload function.
|
||||
*
|
||||
* Since the download cannot be restarted, any partially downloaded data will
|
||||
* not be kept in case the download fails.
|
||||
*
|
||||
* @param aSource
|
||||
* String containing the URI for the download source. Alternatively,
|
||||
|
|
|
@ -35,6 +35,36 @@ function promiseStartDownload(aSourceUrl) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to reach half of its progress, in case it has not
|
||||
* reached the expected progress already.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to wait upon.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the download has reached half of its progress.
|
||||
* @rejects Never.
|
||||
*/
|
||||
function promiseDownloadMidway(aDownload) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
// Wait for the download to reach half of its progress.
|
||||
let onchange = function () {
|
||||
if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
|
||||
aDownload.onchange = null;
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in
|
||||
// case the download already reached the expected progress.
|
||||
aDownload.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to finish, in case it has not finished already.
|
||||
*
|
||||
|
@ -60,6 +90,76 @@ function promiseDownloadStopped(aDownload) {
|
|||
return Promise.reject(aDownload.error || new Error("Download canceled."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a new download, configured to keep partial data, and
|
||||
* returns only when the first part of "interruptible_resumable.txt" has been
|
||||
* saved to disk. You must call "continueResponses" to allow the interruptible
|
||||
* request to continue.
|
||||
*
|
||||
* This function uses either DownloadCopySaver or DownloadLegacySaver based on
|
||||
* the current test run.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The newly created Download object, still in progress.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
function promiseStartDownload_tryToKeepPartialData() {
|
||||
return Task.spawn(function () {
|
||||
mustInterruptResponses();
|
||||
|
||||
// Start a new download and configure it to keep partially downloaded data.
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
|
||||
download = yield Downloads.createDownload({
|
||||
source: httpUrl("interruptible_resumable.txt"),
|
||||
target: { path: targetFilePath,
|
||||
partFilePath: targetFilePath + ".part" },
|
||||
});
|
||||
download.tryToKeepPartialData = true;
|
||||
download.start();
|
||||
} else {
|
||||
// Start a download using nsIExternalHelperAppService, that is configured
|
||||
// to keep partially downloaded data by default.
|
||||
download = yield promiseStartExternalHelperAppServiceDownload();
|
||||
}
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
yield promisePartFileReady(download);
|
||||
|
||||
throw new Task.Result(download);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be called after the progress notification for a download
|
||||
* is received, and waits for the worker thread of BackgroundFileSaver to
|
||||
* receive the data to be written to the ".part" file on disk.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the ".part" file has been written to disk.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
function promisePartFileReady(aDownload) {
|
||||
return Task.spawn(function () {
|
||||
// We don't have control over the file output code in BackgroundFileSaver.
|
||||
// After we receive the download progress notification, we may only check
|
||||
// that the ".part" file has been created, while its size cannot be
|
||||
// determined because the file is currently open.
|
||||
try {
|
||||
do {
|
||||
yield promiseTimeout(50);
|
||||
} while (!(yield OS.File.exists(aDownload.target.partFilePath)));
|
||||
} catch (ex if ex instanceof OS.File.Error) {
|
||||
// This indicates that the file has been created and cannot be accessed.
|
||||
// The specific error might vary with the platform.
|
||||
do_print("Expected exception while checking existence: " + ex.toString());
|
||||
// Wait some more time to allow the write to complete.
|
||||
yield promiseTimeout(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
|
@ -116,7 +216,6 @@ add_task(function test_referrer()
|
|||
function cleanup() {
|
||||
gHttpServer.registerPathHandler(sourcePath, null);
|
||||
}
|
||||
|
||||
do_register_cleanup(cleanup);
|
||||
|
||||
gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
|
||||
|
@ -191,7 +290,7 @@ add_task(function test_initial_final_state()
|
|||
*/
|
||||
add_task(function test_final_state_notified()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
|
@ -206,7 +305,7 @@ add_task(function test_final_state_notified()
|
|||
|
||||
// Allow the download to complete.
|
||||
let promiseAttempt = download.start();
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
yield promiseAttempt;
|
||||
|
||||
// The view should have been notified before the download completes.
|
||||
|
@ -220,26 +319,18 @@ add_task(function test_final_state_notified()
|
|||
*/
|
||||
add_task(function test_intermediate_progress()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let onchange = function () {
|
||||
if (download.progress == 50) {
|
||||
do_check_true(download.hasProgress);
|
||||
do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
|
||||
yield promiseDownloadMidway(download);
|
||||
|
||||
// Continue after the first chunk of data is fully received.
|
||||
deferResponse.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in case
|
||||
// the download already reached the expected progress.
|
||||
download.onchange = onchange;
|
||||
onchange();
|
||||
do_check_true(download.hasProgress);
|
||||
do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
|
||||
|
||||
// Continue after the first chunk of data is fully received.
|
||||
continueResponses();
|
||||
yield promiseDownloadStopped(download);
|
||||
|
||||
do_check_true(download.stopped);
|
||||
|
@ -271,15 +362,30 @@ add_task(function test_empty_progress()
|
|||
*/
|
||||
add_task(function test_empty_noprogress()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
let promiseEmptyRequestReceived = promiseNextRequestReceived();
|
||||
let sourcePath = "/test_empty_noprogress.txt";
|
||||
let sourceUrl = httpUrl("test_empty_noprogress.txt");
|
||||
let deferRequestReceived = Promise.defer();
|
||||
|
||||
// Register an interruptible handler that notifies us when the request occurs.
|
||||
function cleanup() {
|
||||
gHttpServer.registerPathHandler(sourcePath, null);
|
||||
}
|
||||
do_register_cleanup(cleanup);
|
||||
|
||||
registerInterruptibleHandler(sourcePath,
|
||||
function firstPart(aRequest, aResponse) {
|
||||
aResponse.setHeader("Content-Type", "text/plain", false);
|
||||
deferRequestReceived.resolve();
|
||||
}, function secondPart(aRequest, aResponse) { });
|
||||
|
||||
// Start the download, without allowing the request to finish.
|
||||
mustInterruptResponses();
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
// When testing DownloadCopySaver, we have control over the download, thus
|
||||
// we can hook its onchange callback that will be notified when the
|
||||
// download starts.
|
||||
download = yield promiseNewDownload(httpUrl("empty-noprogress.txt"));
|
||||
download = yield promiseNewDownload(sourceUrl);
|
||||
|
||||
download.onchange = function () {
|
||||
if (!download.stopped) {
|
||||
|
@ -294,14 +400,13 @@ add_task(function test_empty_noprogress()
|
|||
// When testing DownloadLegacySaver, the download is already started when it
|
||||
// is created, and it may have already made all needed property change
|
||||
// notifications, thus there is no point in checking the onchange callback.
|
||||
download = yield promiseStartLegacyDownload(
|
||||
httpUrl("empty-noprogress.txt"));
|
||||
download = yield promiseStartLegacyDownload(sourceUrl);
|
||||
}
|
||||
|
||||
// Wait for the request to be received by the HTTP server, but don't allow the
|
||||
// request to finish yet. Before checking the download state, wait for the
|
||||
// events to be processed by the client.
|
||||
yield promiseEmptyRequestReceived;
|
||||
yield deferRequestReceived.promise;
|
||||
yield promiseExecuteSoon();
|
||||
|
||||
// Check that this download has no progress report.
|
||||
|
@ -311,7 +416,7 @@ add_task(function test_empty_noprogress()
|
|||
do_check_eq(download.totalBytes, 0);
|
||||
|
||||
// Now allow the response to finish.
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
yield promiseDownloadStopped(download);
|
||||
|
||||
// Verify the state of the completed download.
|
||||
|
@ -329,8 +434,7 @@ add_task(function test_empty_noprogress()
|
|||
*/
|
||||
add_task(function test_start_twice()
|
||||
{
|
||||
// Ensure that the download cannot complete before start is called twice.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
|
@ -348,7 +452,7 @@ add_task(function test_start_twice()
|
|||
let promiseAttempt2 = download.start();
|
||||
|
||||
// Allow the download to finish.
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
|
||||
// Both promises should now be resolved.
|
||||
yield promiseAttempt1;
|
||||
|
@ -368,7 +472,7 @@ add_task(function test_start_twice()
|
|||
*/
|
||||
add_task(function test_cancel_midway()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
// In this test case, we execute different checks that are only possible with
|
||||
// DownloadCopySaver or DownloadLegacySaver respectively.
|
||||
|
@ -381,62 +485,59 @@ add_task(function test_cancel_midway()
|
|||
options);
|
||||
}
|
||||
|
||||
try {
|
||||
// Cancel the download after receiving the first part of the response.
|
||||
let deferCancel = Promise.defer();
|
||||
let onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
deferCancel.resolve(download.cancel());
|
||||
// Cancel the download after receiving the first part of the response.
|
||||
let deferCancel = Promise.defer();
|
||||
let onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
// Cancel the download immediately during the notification.
|
||||
deferCancel.resolve(download.cancel());
|
||||
|
||||
// The state change happens immediately after calling "cancel", but
|
||||
// temporary files or part files may still exist at this point.
|
||||
do_check_true(download.canceled);
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in
|
||||
// case the download already reached the expected progress. This may happen
|
||||
// when using DownloadLegacySaver.
|
||||
download.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
let promiseAttempt;
|
||||
if (!gUseLegacySaver) {
|
||||
promiseAttempt = download.start();
|
||||
// The state change happens immediately after calling "cancel", but
|
||||
// temporary files or part files may still exist at this point.
|
||||
do_check_true(download.canceled);
|
||||
}
|
||||
};
|
||||
|
||||
// Wait on the promise returned by the "cancel" method to ensure that the
|
||||
// cancellation process finished and temporary files were removed.
|
||||
yield deferCancel.promise;
|
||||
// Register for the notification, but also call the function directly in
|
||||
// case the download already reached the expected progress. This may happen
|
||||
// when using DownloadLegacySaver.
|
||||
download.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
if (gUseLegacySaver) {
|
||||
// The nsIWebBrowserPersist instance should have been canceled now.
|
||||
do_check_eq(options.outPersist.result, Cr.NS_ERROR_ABORT);
|
||||
let promiseAttempt;
|
||||
if (!gUseLegacySaver) {
|
||||
promiseAttempt = download.start();
|
||||
}
|
||||
|
||||
// Wait on the promise returned by the "cancel" method to ensure that the
|
||||
// cancellation process finished and temporary files were removed.
|
||||
yield deferCancel.promise;
|
||||
|
||||
if (gUseLegacySaver) {
|
||||
// The nsIWebBrowserPersist instance should have been canceled now.
|
||||
do_check_eq(options.outPersist.result, Cr.NS_ERROR_ABORT);
|
||||
}
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
|
||||
// Progress properties are not reset by canceling.
|
||||
do_check_eq(download.progress, 50);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
|
||||
do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
|
||||
|
||||
if (!gUseLegacySaver) {
|
||||
// The promise returned by "start" should have been rejected meanwhile.
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
|
||||
// Progress properties are not reset by canceling.
|
||||
do_check_eq(download.progress, 50);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
|
||||
do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
|
||||
|
||||
if (!gUseLegacySaver) {
|
||||
// The promise returned by "start" should have been rejected meanwhile.
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -445,47 +546,35 @@ add_task(function test_cancel_midway()
|
|||
*/
|
||||
add_task(function test_cancel_immediately()
|
||||
{
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
do_check_false(download.stopped);
|
||||
|
||||
let promiseCancel = download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
|
||||
// At this point, we don't know whether the download has already stopped or
|
||||
// is still waiting for cancellation. We can wait on the promise returned
|
||||
// by the "start" method to know for sure.
|
||||
try {
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
do_check_false(download.stopped);
|
||||
|
||||
let promiseCancel = download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
|
||||
// At this point, we don't know whether the download has already stopped or
|
||||
// is still waiting for cancellation. We can wait on the promise returned
|
||||
// by the "start" method to know for sure.
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
|
||||
// Check that the promise returned by the "cancel" method has been resolved.
|
||||
yield promiseCancel;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
|
||||
// Even if we canceled the download immediately, the HTTP request might have
|
||||
// been made, and the internal HTTP handler might be waiting to process it.
|
||||
// Thus, we process any pending events now, to avoid that the request is
|
||||
// processed during the tests that follow, interfering with them.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
yield promiseExecuteSoon();
|
||||
}
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
|
||||
// Check that the promise returned by the "cancel" method has been resolved.
|
||||
yield promiseCancel;
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -493,31 +582,18 @@ add_task(function test_cancel_immediately()
|
|||
*/
|
||||
add_task(function test_cancel_midway_restart()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
let deferCancel = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
deferCancel.resolve(download.cancel());
|
||||
}
|
||||
};
|
||||
download.start();
|
||||
yield deferCancel.promise;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
yield promiseDownloadMidway(download);
|
||||
yield download.cancel();
|
||||
|
||||
do_check_true(download.stopped);
|
||||
|
||||
// The second time, we'll provide the entire interruptible response.
|
||||
continueResponses();
|
||||
download.onchange = null;
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
|
@ -544,22 +620,150 @@ add_task(function test_cancel_midway_restart()
|
|||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download and restarts it from where it stopped.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_tryToKeepPartialData()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.hasPartialData);
|
||||
|
||||
// The target file should not exist, but we should have kept the partial data.
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The second time, we'll request and obtain the second part of the response.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Check that the server now sent the second part only.
|
||||
do_check_eq(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download while keeping partially downloaded data, then removes the
|
||||
* data and restarts the download from the beginning.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_removePartialData()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
do_check_true(download.hasPartialData);
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
yield download.removePartialData();
|
||||
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// The second time, we'll request and obtain the entire response again.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download while keeping partially downloaded data, then removes the
|
||||
* data and restarts the download from the beginning without keeping the partial
|
||||
* data anymore.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_tryToKeepPartialData_false()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
download.tryToKeepPartialData = false;
|
||||
|
||||
// The above property change does not affect existing partial data.
|
||||
do_check_true(download.hasPartialData);
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
yield download.removePartialData();
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// Restart the download from the beginning.
|
||||
mustInterruptResponses();
|
||||
download.start();
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
yield promisePartFileReady(download);
|
||||
|
||||
// While the download is in progress, we should still have a ".part" file.
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_true(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
yield download.cancel();
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
// The ".part" file should be deleted now that the download is canceled.
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// The third time, we'll request and obtain the entire response again.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download right after starting it, then restarts it immediately.
|
||||
*/
|
||||
add_task(function test_cancel_immediately_restart_immediately()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
do_check_false(download.stopped);
|
||||
|
||||
download.cancel();
|
||||
|
@ -578,18 +782,9 @@ add_task(function test_cancel_immediately_restart_immediately()
|
|||
do_check_eq(download.totalBytes, 0);
|
||||
do_check_eq(download.currentBytes, 0);
|
||||
|
||||
// Even if we canceled the download immediately, the HTTP request might have
|
||||
// been made, and the internal HTTP handler might be waiting to process it.
|
||||
// Thus, we process any pending events now, to avoid that the request is
|
||||
// processed during the tests that follow, interfering with them.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
yield promiseExecuteSoon();
|
||||
}
|
||||
|
||||
// Ensure the next request is now allowed to complete, regardless of whether
|
||||
// the canceled request was received by the server or not.
|
||||
deferResponse.resolve();
|
||||
|
||||
continueResponses();
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
|
@ -614,26 +809,13 @@ add_task(function test_cancel_immediately_restart_immediately()
|
|||
*/
|
||||
add_task(function test_cancel_midway_restart_immediately()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
|
||||
let deferMidway = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
do_check_eq(download.progress, 50);
|
||||
deferMidway.resolve();
|
||||
}
|
||||
};
|
||||
let promiseAttempt = download.start();
|
||||
yield deferMidway.promise;
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
|
||||
|
@ -650,9 +832,8 @@ add_task(function test_cancel_midway_restart_immediately()
|
|||
do_check_eq(download.totalBytes, 0);
|
||||
do_check_eq(download.currentBytes, 0);
|
||||
|
||||
deferResponse.resolve();
|
||||
|
||||
// The second request is allowed to complete.
|
||||
continueResponses();
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
|
@ -696,39 +877,96 @@ add_task(function test_cancel_successful()
|
|||
*/
|
||||
add_task(function test_cancel_twice()
|
||||
{
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
do_check_false(download.stopped);
|
||||
|
||||
let promiseCancel1 = download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
let promiseCancel2 = download.cancel();
|
||||
|
||||
try {
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
do_check_false(download.stopped);
|
||||
|
||||
let promiseCancel1 = download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
let promiseCancel2 = download.cancel();
|
||||
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
|
||||
// Both promises should now be resolved.
|
||||
yield promiseCancel1;
|
||||
yield promiseCancel2;
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_false(download.succeeded);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
} catch (ex if ex instanceof Downloads.Error) {
|
||||
do_check_false(ex.becauseSourceFailed);
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
|
||||
// Both promises should now be resolved.
|
||||
yield promiseCancel1;
|
||||
yield promiseCancel2;
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_false(download.succeeded);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that a download cannot be restarted after the "finalize" method.
|
||||
*/
|
||||
add_task(function test_finalize()
|
||||
{
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseFinalized = download.finalize();
|
||||
|
||||
try {
|
||||
yield download.start();
|
||||
do_throw("It should not be possible to restart after finalization.");
|
||||
} catch (ex) { }
|
||||
|
||||
yield promiseFinalized;
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_false(download.succeeded);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the "finalize" method can remove partially downloaded data.
|
||||
*/
|
||||
add_task(function test_finalize_tryToKeepPartialData()
|
||||
{
|
||||
// Check finalization without removing partial data.
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.finalize();
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
do_check_true(download.hasPartialData);
|
||||
do_check_true(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// Clean up.
|
||||
yield download.removePartialData();
|
||||
|
||||
// Check finalization while removing partial data.
|
||||
download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.finalize(true);
|
||||
|
||||
// This time-based solution is a workaround to avoid intermittent failures,
|
||||
// and will be removed when bug 899102 is resolved.
|
||||
if (gUseLegacySaver) {
|
||||
yield promiseTimeout(250);
|
||||
}
|
||||
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -736,26 +974,29 @@ add_task(function test_cancel_twice()
|
|||
*/
|
||||
add_task(function test_whenSucceeded_after_restart()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
mustInterruptResponses();
|
||||
|
||||
let promiseSucceeded;
|
||||
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
// When testing DownloadCopySaver, we have control over the download, thus
|
||||
// we can verify getting a reference before the first download attempt.
|
||||
download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
promiseSucceeded = download.whenSucceeded();
|
||||
download.start();
|
||||
} else {
|
||||
// When testing DownloadLegacySaver, the download is already started when it
|
||||
// is created, thus we cannot get the reference before the first attempt.
|
||||
download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
|
||||
promiseSucceeded = download.whenSucceeded();
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
|
||||
// Get a reference before the first download attempt.
|
||||
let promiseSucceeded = download.whenSucceeded();
|
||||
|
||||
// Cancel the first download attempt.
|
||||
download.start();
|
||||
yield download.cancel();
|
||||
|
||||
deferResponse.resolve();
|
||||
|
||||
// The second request is allowed to complete.
|
||||
continueResponses();
|
||||
download.start();
|
||||
|
||||
// Wait for the download to finish by waiting on the whenSucceeded promise.
|
||||
|
@ -866,21 +1107,25 @@ add_task(function test_error_target()
|
|||
*/
|
||||
add_task(function test_error_restart()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload();
|
||||
|
||||
do_check_true(download.error === null);
|
||||
let download;
|
||||
|
||||
// Create a file without write access permissions before downloading.
|
||||
let targetFile = new FileUtils.File(download.target.path);
|
||||
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
|
||||
targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
|
||||
|
||||
try {
|
||||
yield download.start();
|
||||
// Use DownloadCopySaver or DownloadLegacySaver based on the test run,
|
||||
// specifying the target file we created.
|
||||
if (!gUseLegacySaver) {
|
||||
download = yield Downloads.createDownload({
|
||||
source: httpUrl("source.txt"),
|
||||
target: targetFile,
|
||||
});
|
||||
download.start();
|
||||
} else {
|
||||
download = yield promiseStartLegacyDownload(null,
|
||||
{ targetFile: targetFile });
|
||||
}
|
||||
yield promiseDownloadStopped(download);
|
||||
do_throw("The download should have failed.");
|
||||
} catch (ex if ex instanceof Downloads.Error && ex.becauseTargetFailed) {
|
||||
// A specific error object is thrown when writing to the target fails.
|
||||
|
@ -971,14 +1216,8 @@ add_task(function test_public_and_private()
|
|||
*/
|
||||
add_task(function test_cancel_immediately_restart_and_check_startTime()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
let download = yield promiseStartDownload();
|
||||
|
||||
let download = yield promiseNewDownload();
|
||||
|
||||
download.start();
|
||||
let startTime = download.startTime;
|
||||
do_check_true(isValidDate(download.startTime));
|
||||
|
||||
|
@ -1031,34 +1270,32 @@ add_task(function test_with_content_encoding()
|
|||
*/
|
||||
add_task(function test_cancel_midway_restart_with_content_encoding()
|
||||
{
|
||||
// TODO: Enable all the restart tests for DownloadLegacySaver.
|
||||
if (gUseLegacySaver) {
|
||||
return;
|
||||
}
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible_gzip.txt"));
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible_gzip.txt"));
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
let deferCancel = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled &&
|
||||
download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
|
||||
deferCancel.resolve(download.cancel());
|
||||
}
|
||||
};
|
||||
download.start();
|
||||
yield deferCancel.promise;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
let deferCancel = Promise.defer();
|
||||
let onchange = function () {
|
||||
if (!download.stopped && !download.canceled &&
|
||||
download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
|
||||
deferCancel.resolve(download.cancel());
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in
|
||||
// case the download already reached the expected progress.
|
||||
download.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
yield deferCancel.promise;
|
||||
|
||||
do_check_true(download.stopped);
|
||||
|
||||
// The second time, we'll provide the entire interruptible response.
|
||||
continueResponses();
|
||||
download.onchange = null;
|
||||
yield download.start()
|
||||
yield download.start();
|
||||
|
||||
do_check_eq(download.progress, 100);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
|
||||
|
|
|
@ -42,6 +42,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
|
||||
"@mozilla.org/uriloader/external-helper-app-service;1",
|
||||
Ci.nsIExternalHelperAppService);
|
||||
|
||||
const ServerSocket = Components.Constructor(
|
||||
"@mozilla.org/network/server-socket;1",
|
||||
"nsIServerSocket",
|
||||
|
@ -276,6 +280,68 @@ function promiseStartLegacyDownload(aSourceUrl, aOptions) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new download using the nsIHelperAppService interface, and controls
|
||||
* it using the legacy nsITransfer interface. The source of the download will
|
||||
* be "interruptible_resumable.txt" and partially downloaded data will be kept.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The Download object created as a consequence of controlling the
|
||||
* download through the legacy nsITransfer interface.
|
||||
* @rejects Never. The current test fails in case of exceptions.
|
||||
*/
|
||||
function promiseStartExternalHelperAppServiceDownload() {
|
||||
let sourceURI = NetUtil.newURI(httpUrl("interruptible_resumable.txt"));
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
Downloads.getPublicDownloadList().then(function (aList) {
|
||||
// Temporarily register a view that will get notified when the download we
|
||||
// are controlling becomes visible in the list of downloads.
|
||||
aList.addView({
|
||||
onDownloadAdded: function (aDownload) {
|
||||
aList.removeView(this);
|
||||
|
||||
// Remove the download to keep the list empty for the next test. This
|
||||
// also allows the caller to register the "onchange" event directly.
|
||||
aList.remove(aDownload);
|
||||
|
||||
// When the download object is ready, make it available to the caller.
|
||||
deferred.resolve(aDownload);
|
||||
},
|
||||
});
|
||||
|
||||
let channel = NetUtil.newChannel(sourceURI);
|
||||
|
||||
// Start the actual download process.
|
||||
channel.asyncOpen({
|
||||
contentListener: null,
|
||||
|
||||
onStartRequest: function (aRequest, aContext)
|
||||
{
|
||||
let channel = aRequest.QueryInterface(Ci.nsIChannel);
|
||||
this.contentListener = gExternalHelperAppService.doContent(
|
||||
channel.contentType, aRequest, null, true);
|
||||
this.contentListener.onStartRequest(aRequest, aContext);
|
||||
},
|
||||
|
||||
onStopRequest: function (aRequest, aContext, aStatusCode)
|
||||
{
|
||||
this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
|
||||
},
|
||||
|
||||
onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
|
||||
aCount)
|
||||
{
|
||||
this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
|
||||
aOffset, aCount);
|
||||
},
|
||||
}, null);
|
||||
}.bind(this)).then(null, do_report_unexpected_exception);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new public DownloadList object.
|
||||
*
|
||||
|
@ -317,22 +383,33 @@ function promiseNewPrivateDownloadList() {
|
|||
*/
|
||||
function promiseVerifyContents(aPath, aExpectedContents)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let file = new FileUtils.File(aPath);
|
||||
NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
|
||||
do_check_true(Components.isSuccessCode(aStatus));
|
||||
let contents = NetUtil.readInputStreamToString(aInputStream,
|
||||
aInputStream.available());
|
||||
if (contents.length <= TEST_DATA_SHORT.length * 2) {
|
||||
do_check_eq(contents, aExpectedContents);
|
||||
} else {
|
||||
// Do not print the entire content string to the test log.
|
||||
do_check_eq(contents.length, aExpectedContents.length);
|
||||
do_check_true(contents == aExpectedContents);
|
||||
return Task.spawn(function() {
|
||||
let file = new FileUtils.File(aPath);
|
||||
|
||||
if (!(yield OS.File.exists(aPath))) {
|
||||
do_throw("File does not exist: " + aPath);
|
||||
}
|
||||
deferred.resolve();
|
||||
|
||||
if ((yield OS.File.stat(aPath)).size == 0) {
|
||||
do_throw("File is empty: " + aPath);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
|
||||
do_check_true(Components.isSuccessCode(aStatus));
|
||||
let contents = NetUtil.readInputStreamToString(aInputStream,
|
||||
aInputStream.available());
|
||||
if (contents.length <= TEST_DATA_SHORT.length * 2) {
|
||||
do_check_eq(contents, aExpectedContents);
|
||||
} else {
|
||||
// Do not print the entire content string to the test log.
|
||||
do_check_eq(contents.length, aExpectedContents.length);
|
||||
do_check_true(contents == aExpectedContents);
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -388,55 +465,47 @@ function startFakeServer()
|
|||
}
|
||||
|
||||
/**
|
||||
* This function allows testing events or actions that need to happen in the
|
||||
* middle of a download.
|
||||
* This is an internal reference that should not be used directly by tests.
|
||||
*/
|
||||
let _gDeferResponses = Promise.defer();
|
||||
|
||||
/**
|
||||
* Ensures that all the interruptible requests started after this function is
|
||||
* called won't complete until the continueResponses function is called.
|
||||
*
|
||||
* Normally, the internal HTTP server returns all the available data as soon as
|
||||
* a request is received. In order for some requests to be served one part at a
|
||||
* time, special interruptible handlers are registered on the HTTP server.
|
||||
*
|
||||
* Before making a request to one of the addresses served by the interruptible
|
||||
* handlers, you may call "deferNextResponse" to get a reference to an object
|
||||
* that allows you to control the next request.
|
||||
* time, special interruptible handlers are registered on the HTTP server. This
|
||||
* allows testing events or actions that need to happen in the middle of a
|
||||
* download.
|
||||
*
|
||||
* For example, the handler accessible at the httpUri("interruptible.txt")
|
||||
* address returns the TEST_DATA_SHORT text, then waits until the "resolve"
|
||||
* method is called on the object returned by the function. At this point, the
|
||||
* handler sends the TEST_DATA_SHORT text again to complete the response.
|
||||
* address returns the TEST_DATA_SHORT text, then it may block until the
|
||||
* continueResponses method is called. At this point, the handler sends the
|
||||
* TEST_DATA_SHORT text again to complete the response.
|
||||
*
|
||||
* You can also call the "reject" method on the returned object to interrupt the
|
||||
* response midway. Because of how the network layer is implemented, this does
|
||||
* not cause the socket to return an error.
|
||||
*
|
||||
* @returns Deferred object used to control the response.
|
||||
* If an interruptible request is started before the function is called, it may
|
||||
* or may not be blocked depending on the actual sequence of events.
|
||||
*/
|
||||
function deferNextResponse()
|
||||
function mustInterruptResponses()
|
||||
{
|
||||
do_print("Interruptible request will be controlled.");
|
||||
// If there are pending blocked requests, allow them to complete. This is
|
||||
// done to prevent requests from being blocked forever, but should not affect
|
||||
// the test logic, since previously started requests should not be monitored
|
||||
// on the client side anymore.
|
||||
_gDeferResponses.resolve();
|
||||
|
||||
// Store an internal reference that should not be used directly by tests.
|
||||
if (!deferNextResponse._deferred) {
|
||||
deferNextResponse._deferred = Promise.defer();
|
||||
}
|
||||
return deferNextResponse._deferred;
|
||||
do_print("Interruptible responses will be blocked midway.");
|
||||
_gDeferResponses = Promise.defer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that is resolved when the next interruptible response
|
||||
* handler has received the request, and has started sending the first part of
|
||||
* the response. The response might not have been received by the client yet.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the next request has been received.
|
||||
* @rejects Never.
|
||||
* Allows all the current and future interruptible requests to complete.
|
||||
*/
|
||||
function promiseNextRequestReceived()
|
||||
function continueResponses()
|
||||
{
|
||||
do_print("Requested notification when interruptible request is received.");
|
||||
|
||||
// Store an internal reference that should not be used directly by tests.
|
||||
promiseNextRequestReceived._deferred = Promise.defer();
|
||||
return promiseNextRequestReceived._deferred.promise;
|
||||
do_print("Interruptible responses are now allowed to continue.");
|
||||
_gDeferResponses.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -448,44 +517,24 @@ function promiseNextRequestReceived()
|
|||
* This function is called when the response is received, with the
|
||||
* aRequest and aResponse arguments of the server.
|
||||
* @param aSecondPartFn
|
||||
* This function is called after the "resolve" method of the object
|
||||
* returned by deferNextResponse is called. This function is called with
|
||||
* the aRequest and aResponse arguments of the server.
|
||||
* This function is called with the aRequest and aResponse arguments of
|
||||
* the server, when the continueResponses function is called.
|
||||
*/
|
||||
function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
|
||||
{
|
||||
gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
|
||||
// Get a reference to the controlling object for this request. If the
|
||||
// deferNextResponse function was not called, interrupt the test.
|
||||
let deferResponse = deferNextResponse._deferred;
|
||||
deferNextResponse._deferred = null;
|
||||
if (deferResponse) {
|
||||
do_print("Interruptible request started under control.");
|
||||
} else {
|
||||
do_print("Interruptible request started without being controlled.");
|
||||
deferResponse = Promise.defer();
|
||||
deferResponse.resolve();
|
||||
}
|
||||
do_print("Interruptible request started.");
|
||||
|
||||
// Process the first part of the response.
|
||||
aResponse.processAsync();
|
||||
aFirstPartFn(aRequest, aResponse);
|
||||
|
||||
if (promiseNextRequestReceived._deferred) {
|
||||
do_print("Notifying that interruptible request has been received.");
|
||||
promiseNextRequestReceived._deferred.resolve();
|
||||
promiseNextRequestReceived._deferred = null;
|
||||
}
|
||||
|
||||
// Wait on the deferred object, then finish or abort the request.
|
||||
deferResponse.promise.then(function RIH_onSuccess() {
|
||||
// Wait on the current deferred object, then finish the request.
|
||||
_gDeferResponses.promise.then(function RIH_onSuccess() {
|
||||
aSecondPartFn(aRequest, aResponse);
|
||||
aResponse.finish();
|
||||
do_print("Interruptible request finished.");
|
||||
}, function RIH_onFailure() {
|
||||
aResponse.abort();
|
||||
do_print("Interruptible request aborted.");
|
||||
});
|
||||
}).then(null, Cu.reportError);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -499,6 +548,12 @@ function isValidDate(aDate) {
|
|||
return aDate && aDate.getTime && !isNaN(aDate.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Position of the first byte served by the "interruptible_resumable.txt"
|
||||
* handler during the most recent response.
|
||||
*/
|
||||
let gMostRecentFirstBytePos;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Initialization functions common to all tests
|
||||
|
||||
|
@ -519,11 +574,48 @@ add_task(function test_common_initialize()
|
|||
aResponse.write(TEST_DATA_SHORT);
|
||||
});
|
||||
|
||||
registerInterruptibleHandler("/empty-noprogress.txt",
|
||||
registerInterruptibleHandler("/interruptible_resumable.txt",
|
||||
function firstPart(aRequest, aResponse) {
|
||||
aResponse.setHeader("Content-Type", "text/plain", false);
|
||||
}, function secondPart(aRequest, aResponse) { });
|
||||
|
||||
// Determine if only part of the data should be sent.
|
||||
let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
|
||||
if (aRequest.hasHeader("Range")) {
|
||||
var matches = aRequest.getHeader("Range")
|
||||
.match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
|
||||
var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
|
||||
var lastBytePos = (matches[2] === undefined) ? data.length - 1
|
||||
: matches[2];
|
||||
if (firstBytePos >= data.length) {
|
||||
aResponse.setStatusLine(aRequest.httpVersion, 416,
|
||||
"Requested Range Not Satisfiable");
|
||||
aResponse.setHeader("Content-Range", "*/" + data.length, false);
|
||||
aResponse.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
|
||||
aResponse.setHeader("Content-Range", firstBytePos + "-" +
|
||||
lastBytePos + "/" +
|
||||
data.length, false);
|
||||
|
||||
data = data.substring(firstBytePos, lastBytePos + 1);
|
||||
|
||||
gMostRecentFirstBytePos = firstBytePos;
|
||||
} else {
|
||||
gMostRecentFirstBytePos = 0;
|
||||
}
|
||||
|
||||
aResponse.setHeader("Content-Length", "" + data.length, false);
|
||||
|
||||
aResponse.write(data.substring(0, data.length / 2));
|
||||
|
||||
// Store the second part of the data on the response object, so that it
|
||||
// can be used by the secondPart function.
|
||||
aResponse.secondPartData = data.substring(data.length / 2);
|
||||
}, function secondPart(aRequest, aResponse) {
|
||||
aResponse.write(aResponse.secondPartData);
|
||||
});
|
||||
|
||||
registerInterruptibleHandler("/interruptible_gzip.txt",
|
||||
function firstPart(aRequest, aResponse) {
|
||||
|
@ -549,4 +641,61 @@ add_task(function test_common_initialize()
|
|||
DownloadIntegration._deferTestOpenFile = Promise.defer();
|
||||
DownloadIntegration._deferTestShowDir = Promise.defer();
|
||||
|
||||
// Get a reference to nsIComponentRegistrar, and ensure that is is freed
|
||||
// before the XPCOM shutdown.
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
do_register_cleanup(() => registrar = null);
|
||||
|
||||
// Make sure that downloads started using nsIExternalHelperAppService are
|
||||
// saved to disk without asking for a destination interactively.
|
||||
let mockFactory = {
|
||||
createInstance: function (aOuter, aIid) {
|
||||
return {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
|
||||
promptForSaveToFile: function (aLauncher, aWindowContext,
|
||||
aDefaultFileName,
|
||||
aSuggestedFileExtension,
|
||||
aForcePrompt)
|
||||
{
|
||||
throw new Components.Exception(
|
||||
"Synchronous promptForSaveToFile not implemented.",
|
||||
Cr.NS_ERROR_NOT_AVAILABLE);
|
||||
},
|
||||
promptForSaveToFileAsync: function (aLauncher, aWindowContext,
|
||||
aDefaultFileName,
|
||||
aSuggestedFileExtension,
|
||||
aForcePrompt)
|
||||
{
|
||||
let file = getTempFile(TEST_TARGET_FILE_NAME);
|
||||
aLauncher.saveDestinationAvailable(file);
|
||||
},
|
||||
}.QueryInterface(aIid);
|
||||
}
|
||||
};
|
||||
|
||||
let contractID = "@mozilla.org/helperapplauncherdialog;1";
|
||||
let cid = registrar.contractIDToCID(contractID);
|
||||
let oldFactory = Components.manager.getClassObject(Cc[contractID],
|
||||
Ci.nsIFactory);
|
||||
|
||||
registrar.unregisterFactory(cid, oldFactory);
|
||||
registrar.registerFactory(cid, "", contractID, mockFactory);
|
||||
do_register_cleanup(function () {
|
||||
registrar.unregisterFactory(cid, mockFactory);
|
||||
registrar.registerFactory(cid, "", contractID, oldFactory);
|
||||
});
|
||||
|
||||
// We must also make sure that nsIExternalHelperAppService uses the
|
||||
// JavaScript implementation of nsITransfer, because the
|
||||
// "@mozilla.org/transfer;1" contract is currently implemented in
|
||||
// "toolkit/components/downloads". When the other folder is not included in
|
||||
// builds anymore (bug 851471), we'll not need to do this anymore.
|
||||
let transferContractID = "@mozilla.org/transfer;1";
|
||||
let transferNewCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
|
||||
let transferCid = registrar.contractIDToCID(transferContractID);
|
||||
|
||||
registrar.registerFactory(transferNewCid, "", transferContractID, null);
|
||||
do_register_cleanup(function () {
|
||||
registrar.registerFactory(transferCid, "", transferContractID, null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -56,6 +56,10 @@ add_task(function test_save_reload()
|
|||
target: getTempFile(TEST_TARGET_FILE_NAME),
|
||||
}));
|
||||
|
||||
let legacyDownload = yield promiseStartLegacyDownload();
|
||||
yield legacyDownload.cancel();
|
||||
listForSave.add(legacyDownload);
|
||||
|
||||
yield storeForSave.save();
|
||||
yield storeForLoad.load();
|
||||
|
||||
|
@ -76,8 +80,8 @@ add_task(function test_save_reload()
|
|||
itemsForLoad[i].source.referrer);
|
||||
do_check_eq(itemsForSave[i].target.path,
|
||||
itemsForLoad[i].target.path);
|
||||
do_check_eq(itemsForSave[i].saver.type,
|
||||
itemsForLoad[i].saver.type);
|
||||
do_check_eq(itemsForSave[i].saver.toSerializable(),
|
||||
itemsForLoad[i].saver.toSerializable());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
|
|||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
@ -513,6 +514,7 @@ BookmarkExporter.prototype = {
|
|||
createInstance(Ci.nsIFileOutputStream);
|
||||
safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
|
||||
FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0);
|
||||
let nodeCount;
|
||||
|
||||
try {
|
||||
// We need a buffered output stream for performance. See bug 202477.
|
||||
|
@ -525,7 +527,7 @@ BookmarkExporter.prototype = {
|
|||
createInstance(Ci.nsIConverterOutputStream);
|
||||
this._converterOut.init(bufferedOut, "utf-8", 0, 0);
|
||||
try {
|
||||
yield this._writeContentToFile();
|
||||
nodeCount = yield this._writeContentToFile();
|
||||
|
||||
// Flush the buffer and retain the target file on success only.
|
||||
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
|
||||
|
@ -539,27 +541,34 @@ BookmarkExporter.prototype = {
|
|||
} finally {
|
||||
safeFileOut.close();
|
||||
}
|
||||
throw new Task.Result(nodeCount);
|
||||
},
|
||||
|
||||
_writeContentToFile: function BE__writeContentToFile() {
|
||||
// Weep over stream interface variance.
|
||||
let streamProxy = {
|
||||
converter: this._converterOut,
|
||||
write: function(aData, aLen) {
|
||||
this.converter.writeString(aData);
|
||||
}
|
||||
};
|
||||
return Task.spawn(function() {
|
||||
// Weep over stream interface variance.
|
||||
let streamProxy = {
|
||||
converter: this._converterOut,
|
||||
write: function(aData, aLen) {
|
||||
this.converter.writeString(aData);
|
||||
}
|
||||
};
|
||||
|
||||
// Get list of itemIds that must be excluded from the backup.
|
||||
let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
|
||||
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
|
||||
let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
|
||||
false).root;
|
||||
// Get list of itemIds that must be excluded from the backup.
|
||||
let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
|
||||
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
|
||||
let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
|
||||
false).root;
|
||||
// Serialize to JSON and write to stream.
|
||||
let nodeCount = yield BookmarkNode.serializeAsJSONToOutputStream(root,
|
||||
streamProxy,
|
||||
false,
|
||||
false,
|
||||
excludeItems);
|
||||
root.containerOpen = false;
|
||||
|
||||
// Serialize to JSON and write to stream.
|
||||
yield BookmarkNode.serializeAsJSONToOutputStream(root, streamProxy, false, false,
|
||||
excludeItems);
|
||||
root.containerOpen = false;
|
||||
throw new Task.Result(nodeCount);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -582,6 +591,7 @@ let BookmarkNode = {
|
|||
* @param aExcludeItems
|
||||
* An array of item ids that should not be written to the backup.
|
||||
* @returns Task promise
|
||||
* @resolves the number of serialized uri nodes.
|
||||
*/
|
||||
serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
|
||||
aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
|
||||
|
@ -589,13 +599,17 @@ let BookmarkNode = {
|
|||
return Task.spawn(function() {
|
||||
// Serialize to stream
|
||||
let array = [];
|
||||
if (yield this._appendConvertedNode(aNode, null, array, aIsUICommand,
|
||||
aResolveShortcuts, aExcludeItems)) {
|
||||
let result = yield this._appendConvertedNode(aNode, null, array,
|
||||
aIsUICommand,
|
||||
aResolveShortcuts,
|
||||
aExcludeItems);
|
||||
if (result.appendedNode) {
|
||||
let json = JSON.stringify(array[0]);
|
||||
aStream.write(json, json.length);
|
||||
} else {
|
||||
throw Cr.NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
throw new Task.Result(result.nodeCount);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
@ -603,6 +617,7 @@ let BookmarkNode = {
|
|||
bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
|
||||
return Task.spawn(function() {
|
||||
let node = {};
|
||||
let nodeCount = 0;
|
||||
|
||||
// Set index in order received
|
||||
// XXX handy shortcut, but are there cases where we don't want
|
||||
|
@ -618,7 +633,7 @@ let BookmarkNode = {
|
|||
if (PlacesUtils.nodeIsURI(bNode)) {
|
||||
// Tag root accept only folder nodes
|
||||
if (parent && parent.itemId == PlacesUtils.tagsFolderId)
|
||||
throw new Task.Result(false);
|
||||
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
|
||||
|
||||
// Check for url validity, since we can't halt while writing a backup.
|
||||
// This will throw if we try to serialize an invalid url and it does
|
||||
|
@ -626,14 +641,15 @@ let BookmarkNode = {
|
|||
try {
|
||||
NetUtil.newURI(bNode.uri);
|
||||
} catch (ex) {
|
||||
throw new Task.Result(false);
|
||||
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
|
||||
}
|
||||
|
||||
yield this._addURIProperties(bNode, node, aIsUICommand);
|
||||
nodeCount++;
|
||||
} else if (PlacesUtils.nodeIsContainer(bNode)) {
|
||||
// Tag containers accept only uri nodes
|
||||
if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
|
||||
throw new Task.Result(false);
|
||||
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
|
||||
|
||||
this._addContainerProperties(bNode, node, aIsUICommand,
|
||||
aResolveShortcuts);
|
||||
|
@ -642,18 +658,23 @@ let BookmarkNode = {
|
|||
// Tag containers accept only uri nodes
|
||||
if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
|
||||
(grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
|
||||
throw new Task.Result(false);
|
||||
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
|
||||
|
||||
this._addSeparatorProperties(bNode, node);
|
||||
}
|
||||
|
||||
if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
|
||||
throw new Task.Result(yield this._appendConvertedComplexNode(node, bNode, aArray, aIsUICommand,
|
||||
aResolveShortcuts, aExcludeItems));
|
||||
nodeCount += yield this._appendConvertedComplexNode(node,
|
||||
bNode,
|
||||
aArray,
|
||||
aIsUICommand,
|
||||
aResolveShortcuts,
|
||||
aExcludeItems)
|
||||
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
|
||||
}
|
||||
|
||||
aArray.push(node);
|
||||
throw new Task.Result(true);
|
||||
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
@ -765,6 +786,7 @@ let BookmarkNode = {
|
|||
aExcludeItems) {
|
||||
return Task.spawn(function() {
|
||||
let repr = {};
|
||||
let nodeCount = 0;
|
||||
|
||||
for (let [name, value] in Iterator(aNode))
|
||||
repr[name] = value;
|
||||
|
@ -781,16 +803,17 @@ let BookmarkNode = {
|
|||
let childNode = aSourceNode.getChild(i);
|
||||
if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
|
||||
continue;
|
||||
yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
|
||||
aIsUICommand, aResolveShortcuts,
|
||||
aExcludeItems);
|
||||
let result = yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
|
||||
aIsUICommand, aResolveShortcuts,
|
||||
aExcludeItems);
|
||||
nodeCount += result.nodeCount;
|
||||
}
|
||||
if (!wasOpen)
|
||||
aSourceNode.containerOpen = false;
|
||||
}
|
||||
|
||||
aArray.push(repr);
|
||||
throw new Task.Result(true);
|
||||
throw new Task.Result(nodeCount);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
|
@ -9,11 +9,17 @@ this.EXPORTED_SYMBOLS = ["PlacesBackups"];
|
|||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
||||
Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
this.PlacesBackups = {
|
||||
get _filenamesRegex() {
|
||||
// Get the localized backup filename, will be used to clear out
|
||||
|
@ -24,7 +30,7 @@ this.PlacesBackups = {
|
|||
localizedFilename.substr(0, localizedFilename.indexOf("-"));
|
||||
delete this._filenamesRegex;
|
||||
return this._filenamesRegex =
|
||||
new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)\.(json|html)");
|
||||
new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)(_[0-9]+)*\.(json|html)");
|
||||
},
|
||||
|
||||
get folder() {
|
||||
|
@ -127,6 +133,7 @@ this.PlacesBackups = {
|
|||
* @param aFile
|
||||
* nsIFile where to save JSON backup.
|
||||
* @return {Promise}
|
||||
* @resolves the number of serialized uri nodes.
|
||||
*/
|
||||
saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFile) {
|
||||
return Task.spawn(function() {
|
||||
|
@ -136,7 +143,7 @@ this.PlacesBackups = {
|
|||
throw new Error("Unable to create bookmarks backup file: " + aFile.leafName);
|
||||
}
|
||||
|
||||
yield BookmarkJSONUtils.exportToFile(aFile);
|
||||
let nodeCount = yield BookmarkJSONUtils.exportToFile(aFile);
|
||||
|
||||
if (aFile.parent.equals(this.folder)) {
|
||||
// Update internal cache.
|
||||
|
@ -146,21 +153,25 @@ this.PlacesBackups = {
|
|||
// we also want to copy this new backup to it.
|
||||
// This way we ensure the latest valid backup is the same saved by the
|
||||
// user. See bug 424389.
|
||||
let latestBackup = this.getMostRecent("json");
|
||||
if (!latestBackup || latestBackup != aFile) {
|
||||
let name = this.getFilenameForDate();
|
||||
let name = this.getFilenameForDate();
|
||||
let newFilename = this._appendMetaDataToFilename(name,
|
||||
{ nodeCount: nodeCount });
|
||||
|
||||
let backupFile = yield this._getBackupFileForSameDate(name);
|
||||
if (backupFile) {
|
||||
backupFile.remove(false);
|
||||
} else {
|
||||
let file = this.folder.clone();
|
||||
file.append(name);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
} else {
|
||||
// Update internal cache if we are not replacing an existing
|
||||
// backup file.
|
||||
this.entries.push(file);
|
||||
}
|
||||
aFile.copyTo(this.folder, name);
|
||||
file.append(newFilename);
|
||||
|
||||
// Update internal cache if we are not replacing an existing
|
||||
// backup file.
|
||||
this.entries.push(file);
|
||||
}
|
||||
aFile.copyTo(this.folder, newFilename);
|
||||
}
|
||||
|
||||
throw new Task.Result(nodeCount);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
@ -193,7 +204,8 @@ this.PlacesBackups = {
|
|||
// the total backups after this operation does not exceed the
|
||||
// number specified in the pref.
|
||||
if (!mostRecentBackupFile ||
|
||||
mostRecentBackupFile.leafName != newBackupFilename)
|
||||
!this._isFilenameWithSameDate(mostRecentBackupFile.leafName,
|
||||
newBackupFilename))
|
||||
numberOfBackupsToDelete++;
|
||||
|
||||
while (numberOfBackupsToDelete--) {
|
||||
|
@ -205,20 +217,89 @@ this.PlacesBackups = {
|
|||
// Do nothing if we already have this backup or we don't want backups.
|
||||
if (aMaxBackups === 0 ||
|
||||
(mostRecentBackupFile &&
|
||||
mostRecentBackupFile.leafName == newBackupFilename))
|
||||
this._isFilenameWithSameDate(mostRecentBackupFile.leafName,
|
||||
newBackupFilename)))
|
||||
return;
|
||||
}
|
||||
|
||||
let backupFile = yield this._getBackupFileForSameDate(newBackupFilename);
|
||||
if (backupFile) {
|
||||
if (aForceBackup)
|
||||
backupFile.remove(false);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
// Save bookmarks to a backup file.
|
||||
let newBackupFile = this.folder.clone();
|
||||
newBackupFile.append(newBackupFilename);
|
||||
let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile);
|
||||
|
||||
if (aForceBackup && newBackupFile.exists())
|
||||
newBackupFile.remove(false);
|
||||
// Rename the filename with metadata.
|
||||
let newFilenameWithMetaData = this._appendMetaDataToFilename(
|
||||
newBackupFile.leafName,
|
||||
{ nodeCount: nodeCount });
|
||||
newBackupFile.moveTo(this.folder, newFilenameWithMetaData);
|
||||
|
||||
if (newBackupFile.exists())
|
||||
return;
|
||||
|
||||
yield this.saveBookmarksToJSONFile(newBackupFile);
|
||||
// Update internal cache.
|
||||
let newFileWithMetaData = this.folder.clone();
|
||||
newFileWithMetaData.append(newFilenameWithMetaData);
|
||||
this.entries.pop();
|
||||
this.entries.push(newFileWithMetaData);
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
_appendMetaDataToFilename:
|
||||
function PB__appendMetaDataToFilename(aFilename, aMetaData) {
|
||||
let matches = aFilename.match(this._filenamesRegex);
|
||||
let newFilename = matches[1] + "-" + matches[2] + "_" +
|
||||
aMetaData.nodeCount + "." + matches[4];
|
||||
return newFilename;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the bookmark count for backup file.
|
||||
*
|
||||
* @param aFile
|
||||
* String The backup file.
|
||||
*
|
||||
* @return the bookmark count or null.
|
||||
*/
|
||||
getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFile) {
|
||||
let count = null;
|
||||
let matches = aFile.leafName.match(this._filenamesRegex);
|
||||
|
||||
if (matches && matches[3])
|
||||
count = matches[3].replace(/_/g, "");
|
||||
return count;
|
||||
},
|
||||
|
||||
_isFilenameWithSameDate:
|
||||
function PB__isFilenameWithSameDate(aSourceName, aTargetName) {
|
||||
let sourceMatches = aSourceName.match(this._filenamesRegex);
|
||||
let targetMatches = aTargetName.match(this._filenamesRegex);
|
||||
|
||||
return (sourceMatches && targetMatches &&
|
||||
sourceMatches[1] == targetMatches[1] &&
|
||||
sourceMatches[2] == targetMatches[2] &&
|
||||
sourceMatches[4] == targetMatches[4]);
|
||||
},
|
||||
|
||||
_getBackupFileForSameDate:
|
||||
function PB__getBackupFileForSameDate(aFilename) {
|
||||
return Task.spawn(function() {
|
||||
let iterator = new OS.File.DirectoryIterator(this.folder.path);
|
||||
let backupFile;
|
||||
|
||||
yield iterator.forEach(function(aEntry) {
|
||||
if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
|
||||
backupFile = new FileUtils.File(aEntry.path);
|
||||
return iterator.close();
|
||||
}
|
||||
}.bind(this));
|
||||
yield iterator.close();
|
||||
|
||||
throw new Task.Result(backupFile);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,14 +32,23 @@ function run_test() {
|
|||
|
||||
// Export bookmarks to JSON.
|
||||
var backupFilename = PlacesBackups.getFilenameForDate();
|
||||
var lastBackupFile = bookmarksBackupDir.clone();
|
||||
lastBackupFile.append(backupFilename);
|
||||
if (lastBackupFile.exists())
|
||||
lastBackupFile.remove(false);
|
||||
do_check_false(lastBackupFile.exists());
|
||||
var rx = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
|
||||
var files = bookmarksBackupDir.directoryEntries;
|
||||
var entry;
|
||||
while (files.hasMoreElements()) {
|
||||
entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (entry.leafName.match(rx))
|
||||
entry.remove(false);
|
||||
}
|
||||
|
||||
Task.spawn(function() {
|
||||
yield PlacesBackups.create(NUMBER_OF_BACKUPS);
|
||||
files = bookmarksBackupDir.directoryEntries;
|
||||
while (files.hasMoreElements()) {
|
||||
entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (entry.leafName.match(rx))
|
||||
lastBackupFile = entry;
|
||||
}
|
||||
do_check_true(lastBackupFile.exists());
|
||||
|
||||
// Check that last backup has been retained
|
||||
|
|
|
@ -20,11 +20,16 @@ function run_test() {
|
|||
dateObj.setYear(dateObj.getFullYear() + 1);
|
||||
let name = PlacesBackups.getFilenameForDate(dateObj);
|
||||
do_check_eq(name, "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json");
|
||||
let rx = new RegExp("^" + name.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
|
||||
let files = bookmarksBackupDir.directoryEntries;
|
||||
while (files.hasMoreElements()) {
|
||||
let entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (entry.leafName.match(rx))
|
||||
entry.remove(false);
|
||||
}
|
||||
|
||||
let futureBackupFile = bookmarksBackupDir.clone();
|
||||
futureBackupFile.append(name);
|
||||
if (futureBackupFile.exists())
|
||||
futureBackupFile.remove(false);
|
||||
do_check_false(futureBackupFile.exists());
|
||||
futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
|
||||
do_check_true(futureBackupFile.exists());
|
||||
|
||||
|
@ -36,8 +41,9 @@ function run_test() {
|
|||
do_check_eq(PlacesBackups.entries.length, 1);
|
||||
let mostRecentBackupFile = PlacesBackups.getMostRecent();
|
||||
do_check_neq(mostRecentBackupFile, null);
|
||||
let todayName = PlacesBackups.getFilenameForDate();
|
||||
do_check_eq(mostRecentBackupFile.leafName, todayName);
|
||||
let todayFilename = PlacesBackups.getFilenameForDate();
|
||||
rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
|
||||
do_check_true(mostRecentBackupFile.leafName.match(rx).length > 0);
|
||||
|
||||
// Check that future backup has been removed.
|
||||
do_check_false(futureBackupFile.exists());
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* To confirm that metadata i.e. bookmark count is set and retrieved for
|
||||
* automatic backups.
|
||||
*/
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_saveBookmarksToJSONFile_and_create()
|
||||
{
|
||||
// Add a bookmark
|
||||
let uri = NetUtil.newURI("http://getfirefox.com/");
|
||||
let bookmarkId =
|
||||
PlacesUtils.bookmarks.insertBookmark(
|
||||
PlacesUtils.unfiledBookmarksFolderId, uri,
|
||||
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
|
||||
|
||||
// Test saveBookmarksToJSONFile()
|
||||
let backupFile = FileUtils.getFile("TmpD", ["bookmarks.json"]);
|
||||
backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
|
||||
|
||||
let nodeCount = yield PlacesBackups.saveBookmarksToJSONFile(backupFile, true);
|
||||
do_check_true(nodeCount > 0);
|
||||
do_check_true(backupFile.exists());
|
||||
do_check_eq(backupFile.leafName, "bookmarks.json");
|
||||
|
||||
// Ensure the backup would be copied to our backups folder when the original
|
||||
// backup is saved somewhere else.
|
||||
let recentBackup = PlacesBackups.getMostRecent();
|
||||
let todayFilename = PlacesBackups.getFilenameForDate();
|
||||
do_check_eq(recentBackup.leafName,
|
||||
todayFilename.replace(/\.json/, "_" + nodeCount + ".json"));
|
||||
|
||||
// Clear all backups in our backups folder.
|
||||
yield PlacesBackups.create(0);
|
||||
do_check_eq(PlacesBackups.entries.length, 0);
|
||||
|
||||
// Test create() which saves bookmarks with metadata on the filename.
|
||||
yield PlacesBackups.create();
|
||||
do_check_eq(PlacesBackups.entries.length, 1);
|
||||
|
||||
mostRecentBackupFile = PlacesBackups.getMostRecent();
|
||||
do_check_neq(mostRecentBackupFile, null);
|
||||
let rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "_([0-9]+)\.json$");
|
||||
let matches = mostRecentBackupFile.leafName.match(rx);
|
||||
do_check_true(matches.length > 0 && parseInt(matches[1]) == nodeCount);
|
||||
|
||||
// Cleanup
|
||||
backupFile.remove(false);
|
||||
yield PlacesBackups.create(0);
|
||||
PlacesUtils.bookmarks.removeItem(bookmarkId);
|
||||
});
|
||||
|
|
@ -29,3 +29,4 @@ tail =
|
|||
[test_675416.js]
|
||||
[test_711914.js]
|
||||
[test_protectRoots.js]
|
||||
[test_818593-store-backup-metadata.js]
|
||||
|
|
|
@ -470,18 +470,22 @@ function check_bookmarks_html() {
|
|||
function create_JSON_backup(aFilename) {
|
||||
if (!aFilename)
|
||||
do_throw("you must pass a filename to create_JSON_backup function");
|
||||
remove_all_JSON_backups();
|
||||
let bookmarksBackupDir = gProfD.clone();
|
||||
bookmarksBackupDir.append("bookmarkbackups");
|
||||
if (!bookmarksBackupDir.exists()) {
|
||||
bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
|
||||
do_check_true(bookmarksBackupDir.exists());
|
||||
}
|
||||
let profileBookmarksJSONFile = bookmarksBackupDir.clone();
|
||||
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
||||
if (profileBookmarksJSONFile.exists()) {
|
||||
profileBookmarksJSONFile.remove();
|
||||
}
|
||||
let bookmarksJSONFile = gTestDir.clone();
|
||||
bookmarksJSONFile.append(aFilename);
|
||||
do_check_true(bookmarksJSONFile.exists());
|
||||
bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
|
||||
let profileBookmarksJSONFile = bookmarksBackupDir.clone();
|
||||
profileBookmarksJSONFile = bookmarksBackupDir.clone();
|
||||
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
||||
do_check_true(profileBookmarksJSONFile.exists());
|
||||
return profileBookmarksJSONFile;
|
||||
|
@ -504,12 +508,30 @@ function remove_all_JSON_backups() {
|
|||
/**
|
||||
* Check a JSON backup file for today exists in the profile folder.
|
||||
*
|
||||
* @param aIsAutomaticBackup The boolean indicates whether it's an automatic
|
||||
* backup.
|
||||
* @return nsIFile object for the file.
|
||||
*/
|
||||
function check_JSON_backup() {
|
||||
let profileBookmarksJSONFile = gProfD.clone();
|
||||
profileBookmarksJSONFile.append("bookmarkbackups");
|
||||
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
||||
function check_JSON_backup(aIsAutomaticBackup) {
|
||||
let profileBookmarksJSONFile;
|
||||
if (aIsAutomaticBackup) {
|
||||
let bookmarksBackupDir = gProfD.clone();
|
||||
bookmarksBackupDir.append("bookmarkbackups");
|
||||
let files = bookmarksBackupDir.directoryEntries;
|
||||
let backup_date = new Date().toLocaleFormat("%Y-%m-%d");
|
||||
let rx = new RegExp("^bookmarks-" + backup_date + "_[0-9]+\.json$");
|
||||
while (files.hasMoreElements()) {
|
||||
let entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (entry.leafName.match(rx)) {
|
||||
profileBookmarksJSONFile = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
profileBookmarksJSONFile = gProfD.clone();
|
||||
profileBookmarksJSONFile.append("bookmarkbackups");
|
||||
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
||||
}
|
||||
do_check_true(profileBookmarksJSONFile.exists());
|
||||
return profileBookmarksJSONFile;
|
||||
}
|
||||
|
|
|
@ -66,23 +66,40 @@ function run_test() {
|
|||
for (var i = 0; i < dates.length; i++) {
|
||||
let backupFilename;
|
||||
let shouldExist;
|
||||
let backupFile;
|
||||
if (i > Math.floor(dates.length/2)) {
|
||||
backupFilename = PREFIX + dates[i] + SUFFIX;
|
||||
let files = bookmarksBackupDir.directoryEntries;
|
||||
let rx = new RegExp("^" + PREFIX + dates[i] + "(_[0-9]+){0,1}" + SUFFIX + "$");
|
||||
while (files.hasMoreElements()) {
|
||||
let entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (entry.leafName.match(rx)) {
|
||||
backupFilename = entry.leafName;
|
||||
backupFile = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
shouldExist = true;
|
||||
}
|
||||
else {
|
||||
backupFilename = LOCALIZED_PREFIX + dates[i] + SUFFIX;
|
||||
backupFile = bookmarksBackupDir.clone();
|
||||
backupFile.append(backupFilename);
|
||||
shouldExist = false;
|
||||
}
|
||||
var backupFile = bookmarksBackupDir.clone();
|
||||
backupFile.append(backupFilename);
|
||||
if (backupFile.exists() != shouldExist)
|
||||
do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
|
||||
}
|
||||
|
||||
// Cleanup backups folder.
|
||||
bookmarksBackupDir.remove(true);
|
||||
do_check_false(bookmarksBackupDir.exists());
|
||||
// XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens
|
||||
// on WIN XP.
|
||||
let files = bookmarksBackupDir.directoryEntries;
|
||||
while (files.hasMoreElements()) {
|
||||
let entry = files.getNext().QueryInterface(Ci.nsIFile);
|
||||
entry.remove(false);
|
||||
}
|
||||
do_check_false(bookmarksBackupDir.directoryEntries.hasMoreElements());
|
||||
|
||||
// Recreate the folder.
|
||||
PlacesBackups.folder;
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ Object.defineProperty(ContentTabActor.prototype, "url", {
|
|||
configurable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(ContentTabActor.prototype, "contentWindow", {
|
||||
Object.defineProperty(ContentTabActor.prototype, "window", {
|
||||
get: function() {
|
||||
return this.browser;
|
||||
},
|
||||
|
|
|
@ -2021,15 +2021,7 @@ var InspectorActor = protocol.ActorClass({
|
|||
this.tabActor = tabActor;
|
||||
},
|
||||
|
||||
get window() {
|
||||
let tabActor = this.tabActor;
|
||||
if (tabActor.browser instanceof Ci.nsIDOMWindow) {
|
||||
return tabActor.browser;
|
||||
} else if (tabActor.browser instanceof Ci.nsIDOMElement) {
|
||||
return tabActor.browser.contentWindow;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
get window() this.tabActor.window,
|
||||
|
||||
getWalker: method(function(options={}) {
|
||||
if (this._walkerPromise) {
|
||||
|
|
|
@ -174,6 +174,16 @@ RootActor.prototype = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* This is true for the root actor only, used by some child actors
|
||||
*/
|
||||
get isRootActor() true,
|
||||
|
||||
/**
|
||||
* The (chrome) window, for use by child actors
|
||||
*/
|
||||
get window() Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType),
|
||||
|
||||
/**
|
||||
* Disconnects the actor from the browser window.
|
||||
*/
|
||||
|
|
|
@ -34,18 +34,7 @@ function StyleEditorActor(aConnection, aParentActor)
|
|||
this.conn = aConnection;
|
||||
this._onDocumentLoaded = this._onDocumentLoaded.bind(this);
|
||||
this._onSheetLoaded = this._onSheetLoaded.bind(this);
|
||||
|
||||
if (aParentActor instanceof BrowserTabActor &&
|
||||
aParentActor.browser instanceof Ci.nsIDOMWindow) {
|
||||
this._window = aParentActor.browser;
|
||||
}
|
||||
else if (aParentActor instanceof BrowserTabActor &&
|
||||
aParentActor.browser instanceof Ci.nsIDOMElement) {
|
||||
this._window = aParentActor.browser.contentWindow;
|
||||
}
|
||||
else {
|
||||
this._window = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
}
|
||||
this.parentActor = aParentActor;
|
||||
|
||||
// keep a map of sheets-to-actors so we don't create two actors for one sheet
|
||||
this._sheets = new Map();
|
||||
|
@ -66,19 +55,14 @@ StyleEditorActor.prototype = {
|
|||
conn: null,
|
||||
|
||||
/**
|
||||
* The content window we work with.
|
||||
* The window we work with, taken from the parent actor.
|
||||
*/
|
||||
get win() this._window,
|
||||
get window() this.parentActor.window,
|
||||
|
||||
/**
|
||||
* The current content document of the window we work with.
|
||||
*/
|
||||
get doc() this._window.document,
|
||||
|
||||
/**
|
||||
* A window object, usually the browser window
|
||||
*/
|
||||
_window: null,
|
||||
get document() this.window.document,
|
||||
|
||||
actorPrefix: "styleEditor",
|
||||
|
||||
|
@ -101,7 +85,7 @@ StyleEditorActor.prototype = {
|
|||
|
||||
this.conn.removeActorPool(this._actorPool);
|
||||
this._actorPool = null;
|
||||
this.conn = this._window = null;
|
||||
this.conn = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -120,7 +104,7 @@ StyleEditorActor.prototype = {
|
|||
* @return {object} JSON message to with BaseURI
|
||||
*/
|
||||
onGetBaseURI: function() {
|
||||
return { baseURI: this.doc.baseURIObject };
|
||||
return { baseURI: this.document.baseURIObject };
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -133,11 +117,11 @@ StyleEditorActor.prototype = {
|
|||
|
||||
// Note: listening for load won't be necessary once
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
|
||||
if (this.doc.readyState == "complete") {
|
||||
if (this.document.readyState == "complete") {
|
||||
this._onDocumentLoaded();
|
||||
}
|
||||
else {
|
||||
this.win.addEventListener("load", this._onDocumentLoaded, false);
|
||||
this.window.addEventListener("load", this._onDocumentLoaded, false);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
@ -148,10 +132,10 @@ StyleEditorActor.prototype = {
|
|||
*/
|
||||
_onDocumentLoaded: function(event) {
|
||||
if (event) {
|
||||
this.win.removeEventListener("load", this._onDocumentLoaded, false);
|
||||
this.window.removeEventListener("load", this._onDocumentLoaded, false);
|
||||
}
|
||||
|
||||
let documents = [this.doc];
|
||||
let documents = [this.document];
|
||||
var forms = [];
|
||||
for (let doc of documents) {
|
||||
let sheetForms = this._addStyleSheets(doc.styleSheets);
|
||||
|
@ -267,7 +251,7 @@ StyleEditorActor.prototype = {
|
|||
* @return {object} JSON message with the stylesheet actors' forms
|
||||
*/
|
||||
onGetStyleSheets: function() {
|
||||
let forms = this._addStyleSheets(this.doc.styleSheets);
|
||||
let forms = this._addStyleSheets(this.document.styleSheets);
|
||||
return { "styleSheets": forms };
|
||||
},
|
||||
|
||||
|
@ -295,12 +279,12 @@ StyleEditorActor.prototype = {
|
|||
* Object with 'styelSheet' property for form on new actor.
|
||||
*/
|
||||
onNewStyleSheet: function(request) {
|
||||
let parent = this.doc.documentElement;
|
||||
let style = this.doc.createElementNS("http://www.w3.org/1999/xhtml", "style");
|
||||
let parent = this.document.documentElement;
|
||||
let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
|
||||
style.setAttribute("type", "text/css");
|
||||
|
||||
if (request.text) {
|
||||
style.appendChild(this.doc.createTextNode(request.text));
|
||||
style.appendChild(this.document.createTextNode(request.text));
|
||||
}
|
||||
parent.appendChild(style);
|
||||
|
||||
|
@ -358,16 +342,12 @@ StyleSheetActor.prototype = {
|
|||
/**
|
||||
* Window of target
|
||||
*/
|
||||
get win() {
|
||||
return this.parentActor._window;
|
||||
},
|
||||
get window() this.parentActor.window,
|
||||
|
||||
/**
|
||||
* Document of target.
|
||||
*/
|
||||
get doc() {
|
||||
return this.win.document;
|
||||
},
|
||||
get document() this.window.document,
|
||||
|
||||
/**
|
||||
* Retrieve the index (order) of stylesheet in the document.
|
||||
|
@ -377,8 +357,8 @@ StyleSheetActor.prototype = {
|
|||
get styleSheetIndex()
|
||||
{
|
||||
if (this._styleSheetIndex == -1) {
|
||||
for (let i = 0; i < this.doc.styleSheets.length; i++) {
|
||||
if (this.doc.styleSheets[i] == this.styleSheet) {
|
||||
for (let i = 0; i < this.document.styleSheets.length; i++) {
|
||||
if (this.document.styleSheets[i] == this.styleSheet) {
|
||||
this._styleSheetIndex = i;
|
||||
break;
|
||||
}
|
||||
|
@ -643,9 +623,10 @@ StyleSheetActor.prototype = {
|
|||
};
|
||||
|
||||
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
let loadContext = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsILoadContext);
|
||||
let loadContext = this.window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsILoadContext);
|
||||
channel.setPrivate(loadContext.usePrivateBrowsing);
|
||||
}
|
||||
channel.loadFlags = channel.LOAD_FROM_CACHE;
|
||||
|
@ -684,14 +665,14 @@ StyleSheetActor.prototype = {
|
|||
// it only when all pending StyleEditor-generated transitions ended.
|
||||
if (this._transitionRefCount == 0) {
|
||||
this.styleSheet.insertRule(TRANSITION_RULE, this.styleSheet.cssRules.length);
|
||||
this.doc.documentElement.classList.add(TRANSITION_CLASS);
|
||||
this.document.documentElement.classList.add(TRANSITION_CLASS);
|
||||
}
|
||||
|
||||
this._transitionRefCount++;
|
||||
|
||||
// Set up clean up and commit after transition duration (+10% buffer)
|
||||
// @see _onTransitionEnd
|
||||
this.win.setTimeout(this._onTransitionEnd.bind(this),
|
||||
this.window.setTimeout(this._onTransitionEnd.bind(this),
|
||||
Math.floor(TRANSITION_DURATION_MS * 1.1));
|
||||
},
|
||||
|
||||
|
@ -702,7 +683,7 @@ StyleSheetActor.prototype = {
|
|||
_onTransitionEnd: function()
|
||||
{
|
||||
if (--this._transitionRefCount == 0) {
|
||||
this.doc.documentElement.classList.remove(TRANSITION_CLASS);
|
||||
this.document.documentElement.classList.remove(TRANSITION_CLASS);
|
||||
this.styleSheet.deleteRule(this.styleSheet.cssRules.length - 1);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ addDebuggerToGlobal(this);
|
|||
* Creates a TraceActor. TraceActor provides a stream of function
|
||||
* call/return packets to a remote client gathering a full trace.
|
||||
*/
|
||||
function TraceActor(aConn, aBrowserTabActor)
|
||||
function TraceActor(aConn, aParentActor)
|
||||
{
|
||||
this._attached = false;
|
||||
this._activeTraces = new MapStack();
|
||||
|
@ -30,7 +30,7 @@ function TraceActor(aConn, aBrowserTabActor)
|
|||
}
|
||||
this._sequence = 0;
|
||||
|
||||
this.global = aBrowserTabActor.contentWindow.wrappedJSObject;
|
||||
this.global = aParentActor.window.wrappedJSObject;
|
||||
}
|
||||
|
||||
TraceActor.prototype = {
|
||||
|
|
|
@ -515,7 +515,7 @@ BrowserTabActor.prototype = {
|
|||
// as the title.
|
||||
if (!title && this._tabbrowser) {
|
||||
title = this._tabbrowser
|
||||
._getTabForContentWindow(this.contentWindow).label;
|
||||
._getTabForContentWindow(this.window).label;
|
||||
}
|
||||
return title;
|
||||
},
|
||||
|
@ -530,12 +530,19 @@ BrowserTabActor.prototype = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Getter for the tab content window.
|
||||
* Getter for the tab content window, will be used by child actors to target
|
||||
* the right window.
|
||||
* @return nsIDOMWindow
|
||||
* Tab content window.
|
||||
*/
|
||||
get contentWindow() {
|
||||
return this.browser.contentWindow;
|
||||
get window() {
|
||||
if (this.browser instanceof Ci.nsIDOMWindow) {
|
||||
return this.browser;
|
||||
} else if (this.browser instanceof Ci.nsIDOMElement) {
|
||||
return this.browser.contentWindow;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
grip: function BTA_grip() {
|
||||
|
@ -628,7 +635,7 @@ BrowserTabActor.prototype = {
|
|||
this._contextPool = new ActorPool(this.conn);
|
||||
this.conn.addActorPool(this._contextPool);
|
||||
|
||||
this.threadActor = new ThreadActor(this, this.contentWindow.wrappedJSObject);
|
||||
this.threadActor = new ThreadActor(this, this.window.wrappedJSObject);
|
||||
this._contextPool.addActor(this.threadActor);
|
||||
},
|
||||
|
||||
|
@ -702,7 +709,7 @@ BrowserTabActor.prototype = {
|
|||
// Wait a tick so that the response packet can be dispatched before the
|
||||
// subsequent navigation event packet.
|
||||
Services.tm.currentThread.dispatch(makeInfallible(() => {
|
||||
this.contentWindow.location.reload();
|
||||
this.window.location.reload();
|
||||
}, "BrowserTabActor.prototype.onReload's delayed body"), 0);
|
||||
return {};
|
||||
},
|
||||
|
@ -714,7 +721,7 @@ BrowserTabActor.prototype = {
|
|||
// Wait a tick so that the response packet can be dispatched before the
|
||||
// subsequent navigation event packet.
|
||||
Services.tm.currentThread.dispatch(makeInfallible(() => {
|
||||
this.contentWindow.location = aRequest.url;
|
||||
this.window.location = aRequest.url;
|
||||
}, "BrowserTabActor.prototype.onNavigateTo's delayed body"), 0);
|
||||
return {};
|
||||
},
|
||||
|
@ -727,7 +734,7 @@ BrowserTabActor.prototype = {
|
|||
// The tab is already closed.
|
||||
return;
|
||||
}
|
||||
let windowUtils = this.contentWindow
|
||||
let windowUtils = this.window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
windowUtils.suppressEventHandling(true);
|
||||
|
@ -742,7 +749,7 @@ BrowserTabActor.prototype = {
|
|||
// The tab is already closed.
|
||||
return;
|
||||
}
|
||||
let windowUtils = this.contentWindow
|
||||
let windowUtils = this.window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
windowUtils.resumeTimeouts();
|
||||
|
@ -839,7 +846,7 @@ DebuggerProgressListener.prototype = {
|
|||
|
||||
// Skip non-interesting states.
|
||||
if (!isWindow || !isNetwork ||
|
||||
aProgress.DOMWindow != this._tabActor.contentWindow) {
|
||||
aProgress.DOMWindow != this._tabActor.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -865,7 +872,7 @@ DebuggerProgressListener.prototype = {
|
|||
this._tabActor.threadActor.dbg.enabled = true;
|
||||
}
|
||||
|
||||
let window = this._tabActor.contentWindow;
|
||||
let window = this._tabActor.window;
|
||||
this._tabActor.conn.send({
|
||||
from: this._tabActor.actorID,
|
||||
type: "tabNavigated",
|
||||
|
|
|
@ -53,30 +53,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage",
|
|||
function WebConsoleActor(aConnection, aParentActor)
|
||||
{
|
||||
this.conn = aConnection;
|
||||
|
||||
if (aParentActor.browser instanceof Ci.nsIDOMWindow) {
|
||||
// B2G tab actor |this.browser| points to a DOM window, not
|
||||
// a xul:browser element.
|
||||
//
|
||||
// TODO: bug 802246 - b2g has tab actor which is
|
||||
// not properly supported by the console actor - see bug for details.
|
||||
//
|
||||
// Below we work around the problem: selecting a b2g tab actor
|
||||
// behaves as if the user picked the global console actor.
|
||||
this._window = aParentActor.browser;
|
||||
this._isGlobalActor = true;
|
||||
}
|
||||
else if (aParentActor instanceof BrowserTabActor &&
|
||||
aParentActor.browser instanceof Ci.nsIDOMElement) {
|
||||
// Firefox for desktop tab actor |this.browser| points to the xul:browser
|
||||
// element.
|
||||
this._window = aParentActor.browser.contentWindow;
|
||||
}
|
||||
else {
|
||||
// In all other cases we should behave as the global console actor.
|
||||
this._window = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
this._isGlobalActor = true;
|
||||
}
|
||||
this.parentActor = aParentActor;
|
||||
|
||||
this._actorPool = new ActorPool(this.conn);
|
||||
this.conn.addActorPool(this._actorPool);
|
||||
|
@ -93,7 +70,7 @@ function WebConsoleActor(aConnection, aParentActor)
|
|||
this._onObserverNotification = this._onObserverNotification.bind(this);
|
||||
Services.obs.addObserver(this._onObserverNotification,
|
||||
"inner-window-destroyed", false);
|
||||
if (this._isGlobalActor) {
|
||||
if (this.parentActor.isRootActor) {
|
||||
Services.obs.addObserver(this._onObserverNotification,
|
||||
"last-pb-context-exited", false);
|
||||
}
|
||||
|
@ -108,13 +85,6 @@ WebConsoleActor.prototype =
|
|||
*/
|
||||
dbg: null,
|
||||
|
||||
/**
|
||||
* Tells if this Web Console actor is a global actor or not.
|
||||
* @private
|
||||
* @type boolean
|
||||
*/
|
||||
_isGlobalActor: false,
|
||||
|
||||
/**
|
||||
* Actor pool for all of the actors we send to the client.
|
||||
* @private
|
||||
|
@ -167,9 +137,7 @@ WebConsoleActor.prototype =
|
|||
* The content window we work with.
|
||||
* @type nsIDOMWindow
|
||||
*/
|
||||
get window() this._window,
|
||||
|
||||
_window: null,
|
||||
get window() this.parentActor.window,
|
||||
|
||||
/**
|
||||
* The ConsoleServiceListener instance.
|
||||
|
@ -237,7 +205,7 @@ WebConsoleActor.prototype =
|
|||
this.conn.removeActorPool(this._actorPool);
|
||||
Services.obs.removeObserver(this._onObserverNotification,
|
||||
"inner-window-destroyed");
|
||||
if (this._isGlobalActor) {
|
||||
if (this.parentActor.isRootActor) {
|
||||
Services.obs.removeObserver(this._onObserverNotification,
|
||||
"last-pb-context-exited");
|
||||
}
|
||||
|
@ -248,7 +216,7 @@ WebConsoleActor.prototype =
|
|||
this._dbgGlobals.clear();
|
||||
this.dbg.enabled = false;
|
||||
this.dbg = null;
|
||||
this.conn = this._window = null;
|
||||
this.conn = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -386,7 +354,7 @@ WebConsoleActor.prototype =
|
|||
onStartListeners: function WCA_onStartListeners(aRequest)
|
||||
{
|
||||
let startedListeners = [];
|
||||
let window = !this._isGlobalActor ? this.window : null;
|
||||
let window = !this.parentActor.isRootActor ? this.window : null;
|
||||
|
||||
while (aRequest.listeners.length > 0) {
|
||||
let listener = aRequest.listeners.shift();
|
||||
|
@ -519,7 +487,7 @@ WebConsoleActor.prototype =
|
|||
break;
|
||||
}
|
||||
let cache = this.consoleAPIListener
|
||||
.getCachedMessages(!this._isGlobalActor);
|
||||
.getCachedMessages(!this.parentActor.isRootActor);
|
||||
cache.forEach((aMessage) => {
|
||||
let message = this.prepareConsoleMessageForRemote(aMessage);
|
||||
message._type = type;
|
||||
|
@ -532,7 +500,7 @@ WebConsoleActor.prototype =
|
|||
break;
|
||||
}
|
||||
let cache = this.consoleServiceListener
|
||||
.getCachedMessages(!this._isGlobalActor);
|
||||
.getCachedMessages(!this.parentActor.isRootActor);
|
||||
cache.forEach((aMessage) => {
|
||||
let message = null;
|
||||
if (aMessage instanceof Ci.nsIScriptError) {
|
||||
|
@ -658,10 +626,10 @@ WebConsoleActor.prototype =
|
|||
onClearMessagesCache: function WCA_onClearMessagesCache()
|
||||
{
|
||||
// TODO: Bug 717611 - Web Console clear button does not clear cached errors
|
||||
let windowId = !this._isGlobalActor ?
|
||||
let windowId = !this.parentActor.isRootActor ?
|
||||
WebConsoleUtils.getInnerWindowId(this.window) : null;
|
||||
ConsoleAPIStorage.clearEvents(windowId);
|
||||
if (this._isGlobalActor) {
|
||||
if (this.parentActor.isRootActor) {
|
||||
Services.console.logStringMessage(null); // for the Error Console
|
||||
Services.console.reset();
|
||||
}
|
||||
|
@ -1092,7 +1060,7 @@ WebConsoleActor.prototype =
|
|||
let details = aMessage.request;
|
||||
|
||||
// send request from target's window
|
||||
let request = new this._window.XMLHttpRequest();
|
||||
let request = new this.window.XMLHttpRequest();
|
||||
request.open(details.method, details.url, true);
|
||||
|
||||
for (let {name, value} of details.headers) {
|
||||
|
|
|
@ -126,6 +126,13 @@ var DebuggerServer = {
|
|||
*/
|
||||
_allowConnection: null,
|
||||
|
||||
/**
|
||||
* The windowtype of the chrome window to use for actors that use the global
|
||||
* window (i.e the global style editor). Set this to your main window type,
|
||||
* for example "navigator:browser".
|
||||
*/
|
||||
chromeWindowType: null,
|
||||
|
||||
/**
|
||||
* Prompt the user to accept or decline the incoming connection. This is the
|
||||
* default implementation that products embedding the debugger server may
|
||||
|
@ -282,6 +289,7 @@ var DebuggerServer = {
|
|||
* Install Firefox-specific actors.
|
||||
*/
|
||||
addBrowserActors: function DS_addBrowserActors() {
|
||||
this.chromeWindowType = "navigator:browser";
|
||||
this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
|
||||
this.addActors("resource://gre/modules/devtools/server/actors/script.js");
|
||||
this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
|
||||
|
|
|
@ -67,7 +67,7 @@ TestTabActor.prototype = {
|
|||
constructor: TestTabActor,
|
||||
actorPrefix: "TestTabActor",
|
||||
|
||||
get contentWindow() {
|
||||
get window() {
|
||||
return { wrappedJSObject: this._global };
|
||||
},
|
||||
|
||||
|
|
|
@ -30,3 +30,10 @@ localhost=(local files)
|
|||
# before bug 445704 was fixed. It will be removed
|
||||
# in a subsequent release.
|
||||
bookmarksArchiveFilename=bookmarks-%S.json
|
||||
|
||||
# LOCALIZATION NOTE
|
||||
# The string is used for showing file size of each backup in the "fileRestorePopup" popup
|
||||
# %1$S is the file size
|
||||
# %2$S is the file size unit
|
||||
backupFileSizeText=%1$S %2$S
|
||||
|
||||
|
|
|
@ -0,0 +1,521 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/AddonManager.jsm");
|
||||
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
const KEY_PROFILEDIR = "ProfD";
|
||||
const FILE_DATABASE = "addons.sqlite";
|
||||
const LAST_DB_SCHEMA = 4;
|
||||
|
||||
// Add-on properties present in the columns of the database
|
||||
const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
|
||||
"fullDescription", "developerComments", "eula",
|
||||
"homepageURL", "supportURL", "contributionURL",
|
||||
"contributionAmount", "averageRating", "reviewCount",
|
||||
"reviewURL", "totalDownloads", "weeklyDownloads",
|
||||
"dailyUsers", "sourceURI", "repositoryStatus", "size",
|
||||
"updateDate"];
|
||||
|
||||
|
||||
["LOG", "WARN", "ERROR"].forEach(function(aName) {
|
||||
this.__defineGetter__(aName, function logFuncGetter() {
|
||||
Components.utils.import("resource://gre/modules/AddonLogging.jsm");
|
||||
|
||||
LogManager.getLogger("addons.repository.sqlmigrator", this);
|
||||
return this[aName];
|
||||
});
|
||||
}, this);
|
||||
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
|
||||
|
||||
|
||||
this.AddonRepository_SQLiteMigrator = {
|
||||
|
||||
/**
|
||||
* Migrates data from a previous SQLite version of the
|
||||
* database to the JSON version.
|
||||
*
|
||||
* @param structFunctions an object that contains functions
|
||||
* to create the various objects used
|
||||
* in the new JSON format
|
||||
* @param aCallback A callback to be called when migration
|
||||
* finishes, with the results in an array
|
||||
* @returns bool True if a migration will happen (DB was
|
||||
* found and succesfully opened)
|
||||
*/
|
||||
migrate: function(aCallback) {
|
||||
if (!this._openConnection()) {
|
||||
this._closeConnection();
|
||||
aCallback([]);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG("Importing addon repository from previous " + FILE_DATABASE + " storage.");
|
||||
|
||||
this._retrieveStoredData((results) => {
|
||||
this._closeConnection();
|
||||
let resultArray = [addon for ([,addon] of Iterator(results))];
|
||||
LOG(resultArray.length + " addons imported.")
|
||||
aCallback(resultArray);
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously opens a new connection to the database file.
|
||||
*
|
||||
* @return bool Whether the DB was opened successfully.
|
||||
*/
|
||||
_openConnection: function AD_openConnection() {
|
||||
delete this.connection;
|
||||
|
||||
let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
|
||||
if (!dbfile.exists())
|
||||
return false;
|
||||
|
||||
try {
|
||||
this.connection = Services.storage.openUnsharedDatabase(dbfile);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
|
||||
|
||||
// Any errors in here should rollback
|
||||
try {
|
||||
this.connection.beginTransaction();
|
||||
|
||||
switch (this.connection.schemaVersion) {
|
||||
case 0:
|
||||
return false;
|
||||
|
||||
case 1:
|
||||
LOG("Upgrading database schema to version 2");
|
||||
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
|
||||
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
|
||||
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
|
||||
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
|
||||
case 2:
|
||||
LOG("Upgrading database schema to version 3");
|
||||
this.connection.createTable("compatibility_override",
|
||||
"addon_internal_id INTEGER, " +
|
||||
"num INTEGER, " +
|
||||
"type TEXT, " +
|
||||
"minVersion TEXT, " +
|
||||
"maxVersion TEXT, " +
|
||||
"appID TEXT, " +
|
||||
"appMinVersion TEXT, " +
|
||||
"appMaxVersion TEXT, " +
|
||||
"PRIMARY KEY (addon_internal_id, num)");
|
||||
case 3:
|
||||
LOG("Upgrading database schema to version 4");
|
||||
this.connection.createTable("icon",
|
||||
"addon_internal_id INTEGER, " +
|
||||
"size INTEGER, " +
|
||||
"url TEXT, " +
|
||||
"PRIMARY KEY (addon_internal_id, size)");
|
||||
this._createIndices();
|
||||
this._createTriggers();
|
||||
this.connection.schemaVersion = LAST_DB_SCHEMA;
|
||||
case LAST_DB_SCHEMA:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
this.connection.commitTransaction();
|
||||
} catch (e) {
|
||||
ERROR("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e);
|
||||
this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
|
||||
this.connection.rollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_closeConnection: function() {
|
||||
for each (let stmt in this.asyncStatementsCache)
|
||||
stmt.finalize();
|
||||
this.asyncStatementsCache = {};
|
||||
|
||||
if (this.connection)
|
||||
this.connection.asyncClose();
|
||||
|
||||
delete this.connection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Asynchronously retrieve all add-ons from the database, and pass it
|
||||
* to the specified callback
|
||||
*
|
||||
* @param aCallback
|
||||
* The callback to pass the add-ons back to
|
||||
*/
|
||||
_retrieveStoredData: function AD_retrieveStoredData(aCallback) {
|
||||
let self = this;
|
||||
let addons = {};
|
||||
|
||||
// Retrieve all data from the addon table
|
||||
function getAllAddons() {
|
||||
self.getAsyncStatement("getAllAddons").executeAsync({
|
||||
handleResult: function getAllAddons_handleResult(aResults) {
|
||||
let row = null;
|
||||
while ((row = aResults.getNextRow())) {
|
||||
let internal_id = row.getResultByName("internal_id");
|
||||
addons[internal_id] = self._makeAddonFromAsyncRow(row);
|
||||
}
|
||||
},
|
||||
|
||||
handleError: self.asyncErrorLogger,
|
||||
|
||||
handleCompletion: function getAllAddons_handleCompletion(aReason) {
|
||||
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
ERROR("Error retrieving add-ons from database. Returning empty results");
|
||||
aCallback({});
|
||||
return;
|
||||
}
|
||||
|
||||
getAllDevelopers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve all data from the developer table
|
||||
function getAllDevelopers() {
|
||||
self.getAsyncStatement("getAllDevelopers").executeAsync({
|
||||
handleResult: function getAllDevelopers_handleResult(aResults) {
|
||||
let row = null;
|
||||
while ((row = aResults.getNextRow())) {
|
||||
let addon_internal_id = row.getResultByName("addon_internal_id");
|
||||
if (!(addon_internal_id in addons)) {
|
||||
WARN("Found a developer not linked to an add-on in database");
|
||||
continue;
|
||||
}
|
||||
|
||||
let addon = addons[addon_internal_id];
|
||||
if (!addon.developers)
|
||||
addon.developers = [];
|
||||
|
||||
addon.developers.push(self._makeDeveloperFromAsyncRow(row));
|
||||
}
|
||||
},
|
||||
|
||||
handleError: self.asyncErrorLogger,
|
||||
|
||||
handleCompletion: function getAllDevelopers_handleCompletion(aReason) {
|
||||
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
ERROR("Error retrieving developers from database. Returning empty results");
|
||||
aCallback({});
|
||||
return;
|
||||
}
|
||||
|
||||
getAllScreenshots();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve all data from the screenshot table
|
||||
function getAllScreenshots() {
|
||||
self.getAsyncStatement("getAllScreenshots").executeAsync({
|
||||
handleResult: function getAllScreenshots_handleResult(aResults) {
|
||||
let row = null;
|
||||
while ((row = aResults.getNextRow())) {
|
||||
let addon_internal_id = row.getResultByName("addon_internal_id");
|
||||
if (!(addon_internal_id in addons)) {
|
||||
WARN("Found a screenshot not linked to an add-on in database");
|
||||
continue;
|
||||
}
|
||||
|
||||
let addon = addons[addon_internal_id];
|
||||
if (!addon.screenshots)
|
||||
addon.screenshots = [];
|
||||
addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
|
||||
}
|
||||
},
|
||||
|
||||
handleError: self.asyncErrorLogger,
|
||||
|
||||
handleCompletion: function getAllScreenshots_handleCompletion(aReason) {
|
||||
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
ERROR("Error retrieving screenshots from database. Returning empty results");
|
||||
aCallback({});
|
||||
return;
|
||||
}
|
||||
|
||||
getAllCompatOverrides();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAllCompatOverrides() {
|
||||
self.getAsyncStatement("getAllCompatOverrides").executeAsync({
|
||||
handleResult: function getAllCompatOverrides_handleResult(aResults) {
|
||||
let row = null;
|
||||
while ((row = aResults.getNextRow())) {
|
||||
let addon_internal_id = row.getResultByName("addon_internal_id");
|
||||
if (!(addon_internal_id in addons)) {
|
||||
WARN("Found a compatibility override not linked to an add-on in database");
|
||||
continue;
|
||||
}
|
||||
|
||||
let addon = addons[addon_internal_id];
|
||||
if (!addon.compatibilityOverrides)
|
||||
addon.compatibilityOverrides = [];
|
||||
addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row));
|
||||
}
|
||||
},
|
||||
|
||||
handleError: self.asyncErrorLogger,
|
||||
|
||||
handleCompletion: function getAllCompatOverrides_handleCompletion(aReason) {
|
||||
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
ERROR("Error retrieving compatibility overrides from database. Returning empty results");
|
||||
aCallback({});
|
||||
return;
|
||||
}
|
||||
|
||||
getAllIcons();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAllIcons() {
|
||||
self.getAsyncStatement("getAllIcons").executeAsync({
|
||||
handleResult: function getAllIcons_handleResult(aResults) {
|
||||
let row = null;
|
||||
while ((row = aResults.getNextRow())) {
|
||||
let addon_internal_id = row.getResultByName("addon_internal_id");
|
||||
if (!(addon_internal_id in addons)) {
|
||||
WARN("Found an icon not linked to an add-on in database");
|
||||
continue;
|
||||
}
|
||||
|
||||
let addon = addons[addon_internal_id];
|
||||
let { size, url } = self._makeIconFromAsyncRow(row);
|
||||
addon.icons[size] = url;
|
||||
if (size == 32)
|
||||
addon.iconURL = url;
|
||||
}
|
||||
},
|
||||
|
||||
handleError: self.asyncErrorLogger,
|
||||
|
||||
handleCompletion: function getAllIcons_handleCompletion(aReason) {
|
||||
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
ERROR("Error retrieving icons from database. Returning empty results");
|
||||
aCallback({});
|
||||
return;
|
||||
}
|
||||
|
||||
let returnedAddons = {};
|
||||
for each (let addon in addons)
|
||||
returnedAddons[addon.id] = addon;
|
||||
aCallback(returnedAddons);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Begin asynchronous process
|
||||
getAllAddons();
|
||||
},
|
||||
|
||||
// A cache of statements that are used and need to be finalized on shutdown
|
||||
asyncStatementsCache: {},
|
||||
|
||||
/**
|
||||
* Gets a cached async statement or creates a new statement if it doesn't
|
||||
* already exist.
|
||||
*
|
||||
* @param aKey
|
||||
* A unique key to reference the statement
|
||||
* @return a mozIStorageAsyncStatement for the SQL corresponding to the
|
||||
* unique key
|
||||
*/
|
||||
getAsyncStatement: function AD_getAsyncStatement(aKey) {
|
||||
if (aKey in this.asyncStatementsCache)
|
||||
return this.asyncStatementsCache[aKey];
|
||||
|
||||
let sql = this.queries[aKey];
|
||||
try {
|
||||
return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql);
|
||||
} catch (e) {
|
||||
ERROR("Error creating statement " + aKey + " (" + sql + ")");
|
||||
throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e,
|
||||
e.result);
|
||||
}
|
||||
},
|
||||
|
||||
// The queries used by the database
|
||||
queries: {
|
||||
getAllAddons: "SELECT internal_id, id, type, name, version, " +
|
||||
"creator, creatorURL, description, fullDescription, " +
|
||||
"developerComments, eula, homepageURL, supportURL, " +
|
||||
"contributionURL, contributionAmount, averageRating, " +
|
||||
"reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
|
||||
"dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
|
||||
"FROM addon",
|
||||
|
||||
getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
|
||||
"ORDER BY addon_internal_id, num",
|
||||
|
||||
getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
|
||||
"thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
|
||||
"FROM screenshot ORDER BY addon_internal_id, num",
|
||||
|
||||
getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " +
|
||||
"maxVersion, appID, appMinVersion, appMaxVersion " +
|
||||
"FROM compatibility_override " +
|
||||
"ORDER BY addon_internal_id, num",
|
||||
|
||||
getAllIcons: "SELECT addon_internal_id, size, url FROM icon " +
|
||||
"ORDER BY addon_internal_id, size",
|
||||
},
|
||||
|
||||
/**
|
||||
* Make add-on structure from an asynchronous row.
|
||||
*
|
||||
* @param aRow
|
||||
* The asynchronous row to use
|
||||
* @return The created add-on
|
||||
*/
|
||||
_makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
|
||||
// This is intentionally not an AddonSearchResult object in order
|
||||
// to allow AddonDatabase._parseAddon to parse it, same as if it
|
||||
// was read from the JSON database.
|
||||
|
||||
let addon = { icons: {} };
|
||||
|
||||
for (let prop of PROP_SINGLE) {
|
||||
addon[prop] = aRow.getResultByName(prop)
|
||||
};
|
||||
|
||||
return addon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a developer from an asynchronous row
|
||||
*
|
||||
* @param aRow
|
||||
* The asynchronous row to use
|
||||
* @return The created developer
|
||||
*/
|
||||
_makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
|
||||
let name = aRow.getResultByName("name");
|
||||
let url = aRow.getResultByName("url")
|
||||
return new AddonManagerPrivate.AddonAuthor(name, url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a screenshot from an asynchronous row
|
||||
*
|
||||
* @param aRow
|
||||
* The asynchronous row to use
|
||||
* @return The created screenshot
|
||||
*/
|
||||
_makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
|
||||
let url = aRow.getResultByName("url");
|
||||
let width = aRow.getResultByName("width");
|
||||
let height = aRow.getResultByName("height");
|
||||
let thumbnailURL = aRow.getResultByName("thumbnailURL");
|
||||
let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
|
||||
let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
|
||||
let caption = aRow.getResultByName("caption");
|
||||
return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
|
||||
thumbnailWidth, thumbnailHeight, caption);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a CompatibilityOverride from an asynchronous row
|
||||
*
|
||||
* @param aRow
|
||||
* The asynchronous row to use
|
||||
* @return The created CompatibilityOverride
|
||||
*/
|
||||
_makeCompatOverrideFromAsyncRow: function AD_makeCompatOverrideFromAsyncRow(aRow) {
|
||||
let type = aRow.getResultByName("type");
|
||||
let minVersion = aRow.getResultByName("minVersion");
|
||||
let maxVersion = aRow.getResultByName("maxVersion");
|
||||
let appID = aRow.getResultByName("appID");
|
||||
let appMinVersion = aRow.getResultByName("appMinVersion");
|
||||
let appMaxVersion = aRow.getResultByName("appMaxVersion");
|
||||
return new AddonManagerPrivate.AddonCompatibilityOverride(type,
|
||||
minVersion,
|
||||
maxVersion,
|
||||
appID,
|
||||
appMinVersion,
|
||||
appMaxVersion);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make an icon from an asynchronous row
|
||||
*
|
||||
* @param aRow
|
||||
* The asynchronous row to use
|
||||
* @return An object containing the size and URL of the icon
|
||||
*/
|
||||
_makeIconFromAsyncRow: function AD_makeIconFromAsyncRow(aRow) {
|
||||
let size = aRow.getResultByName("size");
|
||||
let url = aRow.getResultByName("url");
|
||||
return { size: size, url: url };
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to log an SQL error.
|
||||
*
|
||||
* @param aError
|
||||
* The storage error code associated with the error
|
||||
* @param aErrorString
|
||||
* An error message
|
||||
*/
|
||||
logSQLError: function AD_logSQLError(aError, aErrorString) {
|
||||
ERROR("SQL error " + aError + ": " + aErrorString);
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to log any errors that occur during async statements.
|
||||
*
|
||||
* @param aError
|
||||
* A mozIStorageError to log
|
||||
*/
|
||||
asyncErrorLogger: function AD_asyncErrorLogger(aError) {
|
||||
ERROR("Async SQL error " + aError.result + ": " + aError.message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously creates the triggers in the database.
|
||||
*/
|
||||
_createTriggers: function AD__createTriggers() {
|
||||
this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon");
|
||||
this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
|
||||
"ON addon BEGIN " +
|
||||
"DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
|
||||
"DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
|
||||
"DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " +
|
||||
"DELETE FROM icon WHERE addon_internal_id=old.internal_id; " +
|
||||
"END");
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously creates the indices in the database.
|
||||
*/
|
||||
_createIndices: function AD__createIndices() {
|
||||
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
|
||||
"ON developer (addon_internal_id)");
|
||||
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
|
||||
"ON screenshot (addon_internal_id)");
|
||||
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " +
|
||||
"ON compatibility_override (addon_internal_id)");
|
||||
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " +
|
||||
"ON icon (addon_internal_id)");
|
||||
}
|
||||
}
|
|
@ -2973,7 +2973,7 @@ var gDetailView = {
|
|||
var settings = xml.querySelectorAll(":root > setting");
|
||||
|
||||
var firstSetting = null;
|
||||
for (let setting of settings) {
|
||||
for (var setting of settings) {
|
||||
|
||||
var desc = stripTextNodes(setting).trim();
|
||||
if (!setting.hasAttribute("desc"))
|
||||
|
@ -2983,6 +2983,13 @@ var gDetailView = {
|
|||
if (type == "file" || type == "directory")
|
||||
setting.setAttribute("fullpath", "true");
|
||||
|
||||
setting = document.importNode(setting, true);
|
||||
var style = setting.getAttribute("style");
|
||||
if (style) {
|
||||
setting.removeAttribute("style");
|
||||
setting.setAttribute("style", style);
|
||||
}
|
||||
|
||||
rows.appendChild(setting);
|
||||
var visible = window.getComputedStyle(setting, null).getPropertyValue("display") != "none";
|
||||
if (!firstSetting && visible) {
|
||||
|
|
|
@ -29,6 +29,7 @@ EXTRA_PP_COMPONENTS += [
|
|||
EXTRA_JS_MODULES += [
|
||||
'AddonLogging.jsm',
|
||||
'AddonRepository.jsm',
|
||||
'AddonRepository_SQLiteMigrator.jsm',
|
||||
'AddonUpdateChecker.jsm',
|
||||
'ChromeManifestParser.jsm',
|
||||
'DeferredSave.jsm',
|
||||
|
|
|
@ -59,6 +59,7 @@ MOCHITEST_BROWSER_MAIN = \
|
|||
browser_openDialog.js \
|
||||
browser_types.js \
|
||||
browser_inlinesettings.js \
|
||||
browser_inlinesettings_custom.js \
|
||||
browser_inlinesettings_info.js \
|
||||
browser_tabsettings.js \
|
||||
browser_pluginprefs.js \
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0"?>
|
||||
<bindings xmlns="http://www.mozilla.org/xbl"
|
||||
xmlns:xbl="http://www.mozilla.org/xbl"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<binding id="custom"
|
||||
extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
|
||||
<content>
|
||||
<xul:vbox>
|
||||
<xul:hbox class="preferences-alignment">
|
||||
<xul:label anonid="label" class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
|
||||
</xul:hbox>
|
||||
<xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
|
||||
</xul:vbox>
|
||||
<xul:hbox class="preferences-alignment">
|
||||
<xul:label anonid="input" value="Woah!"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
</binding>
|
||||
</bindings>
|
8
toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/bootstrap.js
поставляемый
Normal file
|
@ -0,0 +1,8 @@
|
|||
function install (params, aReason) {
|
||||
}
|
||||
function uninstall (params, aReason) {
|
||||
}
|
||||
function startup (params, aReason) {
|
||||
}
|
||||
function shutdown (params, aReason) {
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
content inlinesettings ./
|
||||
locale inlinesettings en-US ./
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" ?>
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:id>inlinesettings1@tests.mozilla.org</em:id>
|
||||
<em:name>Inline Settings (Bootstrap)</em:name>
|
||||
<em:version>1</em:version>
|
||||
<em:bootstrap>true</em:bootstrap>
|
||||
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<em:id>toolkit@mozilla.org</em:id>
|
||||
<em:minVersion>0</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
</Description>
|
||||
|
||||
</RDF>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE vbox SYSTEM "chrome://inlinesettings/locale/string.dtd">
|
||||
<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<setting type="custom" title="&custom.title;" style="background-color: blue; display: -moz-grid-line; -moz-binding: url('chrome://inlinesettings/content/binding.xml#custom');"/>
|
||||
</vbox>
|
|
@ -0,0 +1 @@
|
|||
<!ENTITY custom.title "Custom">
|
|
@ -0,0 +1,92 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
// Tests various aspects of the details view
|
||||
|
||||
var gManagerWindow;
|
||||
var gCategoryUtilities;
|
||||
|
||||
function installAddon(aCallback) {
|
||||
AddonManager.getInstallForURL(TESTROOT + "addons/browser_inlinesettings1_custom.xpi",
|
||||
function(aInstall) {
|
||||
aInstall.addListener({
|
||||
onInstallEnded: function() {
|
||||
executeSoon(aCallback);
|
||||
}
|
||||
});
|
||||
aInstall.install();
|
||||
}, "application/x-xpinstall");
|
||||
}
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
installAddon(function () {
|
||||
open_manager("addons://list/extension", function(aWindow) {
|
||||
gManagerWindow = aWindow;
|
||||
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function end_test() {
|
||||
close_manager(gManagerWindow, function() {
|
||||
AddonManager.getAddonByID("inlinesettings1@tests.mozilla.org", function(aAddon) {
|
||||
aAddon.uninstall();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Addon with options.xul, with custom <setting> binding
|
||||
add_test(function() {
|
||||
var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
|
||||
is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE, "Options should be inline type");
|
||||
addon.parentNode.ensureElementIsVisible(addon);
|
||||
|
||||
var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
|
||||
is_element_visible(button, "Preferences button should be visible");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// Addon with options.xul, also a test for the setting.xml bindings
|
||||
add_test(function() {
|
||||
var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
|
||||
addon.parentNode.ensureElementIsVisible(addon);
|
||||
|
||||
var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
|
||||
EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
|
||||
|
||||
wait_for_view_load(gManagerWindow, function() {
|
||||
is(gManagerWindow.gViewController.currentViewId,
|
||||
"addons://detail/inlinesettings1%40tests.mozilla.org/preferences",
|
||||
"Current view should scroll to preferences");
|
||||
|
||||
var grid = gManagerWindow.document.getElementById("detail-grid");
|
||||
var settings = grid.querySelectorAll("rows > setting");
|
||||
is(settings.length, 1, "Grid should have settings children");
|
||||
|
||||
ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
|
||||
|
||||
var style = window.getComputedStyle(settings[0], null);
|
||||
is(style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Background color should be set");
|
||||
is(style.getPropertyValue("display"), "-moz-grid-line", "Display should be set");
|
||||
is(style.getPropertyValue("-moz-binding"), 'url("chrome://inlinesettings/content/binding.xml#custom")', "Binding should be set");
|
||||
|
||||
var label = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "label");
|
||||
is(label.textContent, "Custom", "Localized string should be shown");
|
||||
|
||||
var input = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "input");
|
||||
isnot(input, null, "Binding should be applied");
|
||||
is(input.value, "Woah!", "Binding should be applied");
|
||||
|
||||
button = gManagerWindow.document.getElementById("detail-prefs-btn");
|
||||
is_element_hidden(button, "Preferences button should not be visible");
|
||||
|
||||
gCategoryUtilities.openType("extension", run_next_test);
|
||||
});
|
||||
});
|
|
@ -615,7 +615,7 @@ function createInstallRDF(aData) {
|
|||
function writeInstallRDFToDir(aData, aDir, aExtraFile) {
|
||||
var rdf = createInstallRDF(aData);
|
||||
if (!aDir.exists())
|
||||
aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
||||
aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
var file = aDir.clone();
|
||||
file.append("install.rdf");
|
||||
if (file.exists())
|
||||
|
@ -633,7 +633,7 @@ function writeInstallRDFToDir(aData, aDir, aExtraFile) {
|
|||
|
||||
file = aDir.clone();
|
||||
file.append(aExtraFile);
|
||||
file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, 0644);
|
||||
file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -665,7 +665,7 @@ function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
|
|||
}
|
||||
|
||||
if (!dir.exists())
|
||||
dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
||||
dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
dir.append(id + ".xpi");
|
||||
var rdf = createInstallRDF(aData);
|
||||
var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
|
||||
|
@ -718,7 +718,7 @@ function manuallyInstall(aXPIFile, aInstallLocation, aID) {
|
|||
if (TEST_UNPACKED) {
|
||||
let dir = aInstallLocation.clone();
|
||||
dir.append(aID);
|
||||
dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
||||
dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
|
||||
createInstance(AM_Ci.nsIZipReader);
|
||||
zip.open(aXPIFile);
|
||||
|
|
|
@ -20,7 +20,7 @@ const GETADDONS_RESULTS = BASE_URL + "/data/test_AddonRepository_cach
|
|||
const GETADDONS_EMPTY = BASE_URL + "/data/test_AddonRepository_empty.xml";
|
||||
const GETADDONS_FAILED = BASE_URL + "/data/test_AddonRepository_failed.xml";
|
||||
|
||||
const FILE_DATABASE = "addons.sqlite";
|
||||
const FILE_DATABASE = "addons.json";
|
||||
const ADDON_NAMES = ["test_AddonRepository_1",
|
||||
"test_AddonRepository_2",
|
||||
"test_AddonRepository_3"];
|
||||
|
@ -521,6 +521,16 @@ function check_initialized_cache(aExpectedToFind, aCallback) {
|
|||
});
|
||||
}
|
||||
|
||||
// Waits for the data to be written from the in-memory DB to the addons.json
|
||||
// file that is done asynchronously through OS.File
|
||||
function waitForFlushedData(aCallback) {
|
||||
Services.obs.addObserver({
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
Services.obs.removeObserver(this, "addon-repository-data-written");
|
||||
aCallback(aData == "true");
|
||||
}
|
||||
}, "addon-repository-data-written", false);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
// Setup for test
|
||||
|
@ -558,7 +568,8 @@ function run_test_1() {
|
|||
// Tests that the cache and database begin as empty
|
||||
function run_test_2() {
|
||||
check_database_exists(false);
|
||||
check_cache([false, false, false], false, run_test_3);
|
||||
check_cache([false, false, false], false, function(){});
|
||||
waitForFlushedData(run_test_3);
|
||||
}
|
||||
|
||||
// Tests repopulateCache when the search fails
|
||||
|
@ -611,7 +622,9 @@ function run_test_6() {
|
|||
check_database_exists(false);
|
||||
|
||||
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
|
||||
check_cache([false, false, false], false, run_test_7);
|
||||
check_cache([false, false, false], false, function() {});
|
||||
|
||||
waitForFlushedData(run_test_7);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -710,7 +723,7 @@ function run_test_13() {
|
|||
function run_test_14() {
|
||||
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
|
||||
|
||||
trigger_background_update(function() {
|
||||
waitForFlushedData(function() {
|
||||
check_database_exists(true);
|
||||
|
||||
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
|
||||
|
@ -718,6 +731,8 @@ function run_test_14() {
|
|||
do_execute_soon(run_test_15);
|
||||
});
|
||||
});
|
||||
|
||||
trigger_background_update();
|
||||
}
|
||||
|
||||
// Tests that the XPI add-ons correctly use the repository properties when
|
||||
|
|
|
@ -109,7 +109,7 @@ function run_test() {
|
|||
stmt.finalize();
|
||||
|
||||
db.close();
|
||||
run_test_2();
|
||||
do_test_finished();
|
||||
}
|
||||
}, "addon-repository-shutdown", null);
|
||||
|
||||
|
@ -126,37 +126,3 @@ function run_test() {
|
|||
AddonRepository.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
function run_test_2() {
|
||||
// Write out a minimal database.
|
||||
let db = AM_Cc["@mozilla.org/storage/service;1"].
|
||||
getService(AM_Ci.mozIStorageService).
|
||||
openDatabase(dbfile);
|
||||
|
||||
db.createTable("futuristicSchema",
|
||||
"id INTEGER, " +
|
||||
"sharks TEXT, " +
|
||||
"lasers TEXT");
|
||||
|
||||
db.schemaVersion = 1000;
|
||||
db.close();
|
||||
|
||||
Services.obs.addObserver({
|
||||
observe: function () {
|
||||
Services.obs.removeObserver(this, "addon-repository-shutdown");
|
||||
// Check the DB schema has changed once AddonRepository has freed it.
|
||||
db = AM_Cc["@mozilla.org/storage/service;1"].
|
||||
getService(AM_Ci.mozIStorageService).
|
||||
openDatabase(dbfile);
|
||||
do_check_eq(db.schemaVersion, EXPECTED_SCHEMA_VERSION);
|
||||
db.close();
|
||||
do_test_finished();
|
||||
}
|
||||
}, "addon-repository-shutdown", null);
|
||||
|
||||
// Force a connection to the addon database to be opened.
|
||||
Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
|
||||
AddonRepository.getCachedAddonByID("test1@tests.mozilla.org", function (aAddon) {
|
||||
AddonRepository.shutdown();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1961,10 +1961,6 @@ function getProcessArgs(aExtraArgs) {
|
|||
launchScript.create(AUS_Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY);
|
||||
|
||||
let scriptContents = "#! /bin/sh\n";
|
||||
// On Mac OS X versions prior to 10.6 the i386 acrhitecture must be used.
|
||||
if (gIsLessThanMacOSX_10_6) {
|
||||
scriptContents += "arch -arch i386 ";
|
||||
}
|
||||
scriptContents += gAppBinPath + " -no-remote -process-updates " +
|
||||
aExtraArgs.join(" ") + " 1> " +
|
||||
appConsoleLogPath + " 2>&1";
|
||||
|
@ -2034,45 +2030,6 @@ function getLaunchScript() {
|
|||
return launchScript;
|
||||
}
|
||||
|
||||
// A shell script is used to get the OS version due to nsSystemInfo not
|
||||
// returning the actual OS version. It is possible to get the actual OS version
|
||||
// using ctypes but it would be more complicated than using a shell script.
|
||||
XPCOMUtils.defineLazyGetter(this, "gIsLessThanMacOSX_10_6", function test_gMacVer() {
|
||||
if (!IS_MACOSX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [versionScript, versionFile] = getVersionScriptAndFile();
|
||||
// Precreate the script with executable permissions
|
||||
versionScript.create(AUS_Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY);
|
||||
let scriptContents = "#! /bin/sh\nsw_vers -productVersion >> " + versionFile.path;
|
||||
writeFile(versionScript, scriptContents);
|
||||
logTestInfo("created " + versionScript.path + " shell script containing:\n" +
|
||||
scriptContents);
|
||||
|
||||
let versionScriptPath = versionScript.path;
|
||||
if (/ /.test(versionScriptPath)) {
|
||||
versionScriptPath = '"' + versionScriptPath + '"';
|
||||
}
|
||||
|
||||
|
||||
let launchBin = getLaunchBin();
|
||||
let args = [versionScriptPath];
|
||||
let process = AUS_Cc["@mozilla.org/process/util;1"].
|
||||
createInstance(AUS_Ci.nsIProcess);
|
||||
process.init(launchBin);
|
||||
process.run(true, args, args.length);
|
||||
if (process.exitValue != 0) {
|
||||
do_throw("Version script exited with " + process.exitValue + "... unable " +
|
||||
"to get Mac OS X version!");
|
||||
}
|
||||
|
||||
let version = readFile(versionFile).split("\n")[0];
|
||||
logTestInfo("executing on Mac OS X verssion " + version);
|
||||
|
||||
return (Services.vc.compare(version, "10.6") < 0)
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks for the existence of a platform specific application binary that can
|
||||
* be used for the test and gets its path if it is found.
|
||||
|
@ -2263,30 +2220,6 @@ let gTimerCallback = {
|
|||
QueryInterface: XPCOMUtils.generateQI([AUS_Ci.nsITimerCallback])
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the nsIFile references for the shell script to retrieve the Mac OS X
|
||||
* version and the nsIFile to pipe the output of the shell script. If either of
|
||||
* these files exist they will be removed by this function.
|
||||
*
|
||||
* @return array containing two nsIFile references. The first array member is
|
||||
* the nsIFile for the shell script to launch to get the Mac OS X
|
||||
* version and the second array member is the nsIFile for the piped
|
||||
* output from the shell script.
|
||||
*/
|
||||
function getVersionScriptAndFile() {
|
||||
let versionScript = do_get_file("/", true);
|
||||
let versionFile = versionScript.clone();
|
||||
versionScript.append("get_version.sh");
|
||||
if (versionScript.exists()) {
|
||||
versionScript.remove(false);
|
||||
}
|
||||
versionFile.append("version.out");
|
||||
if (versionFile.exists()) {
|
||||
versionFile.remove(false);
|
||||
}
|
||||
return [versionScript, versionFile];
|
||||
}
|
||||
|
||||
// Environment related globals
|
||||
let gShouldResetEnv = undefined;
|
||||
let gAddedEnvXRENoWindowsCrashDialog = false;
|
||||
|
|
|
@ -198,10 +198,6 @@ function end_test() {
|
|||
if (IS_UNIX) {
|
||||
// This will delete the launch script if it exists.
|
||||
getLaunchScript();
|
||||
if (IS_MACOSX) {
|
||||
// This will delete the version script and version file if they exist.
|
||||
getVersionScriptAndFile();
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp();
|
||||
|
|
|
@ -257,10 +257,6 @@ function end_test() {
|
|||
if (IS_UNIX) {
|
||||
// This will delete the launch script if it exists.
|
||||
getLaunchScript();
|
||||
if (IS_MACOSX) {
|
||||
// This will delete the version script and version file if they exist.
|
||||
getVersionScriptAndFile();
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp();
|
||||
|
|
|
@ -223,10 +223,6 @@ function end_test() {
|
|||
if (IS_UNIX) {
|
||||
// This will delete the launch script if it exists.
|
||||
getLaunchScript();
|
||||
if (IS_MACOSX) {
|
||||
// This will delete the version script and version file if they exist.
|
||||
getVersionScriptAndFile();
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp();
|
||||
|
|
|
@ -286,10 +286,6 @@ function end_test() {
|
|||
if (IS_UNIX) {
|
||||
// This will delete the launch script if it exists.
|
||||
getLaunchScript();
|
||||
if (IS_MACOSX) {
|
||||
// This will delete the version script and version file if they exist.
|
||||
getVersionScriptAndFile();
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp();
|
||||
|
|