This commit is contained in:
Ryan VanderMeulen 2013-08-01 20:25:21 -04:00
Родитель 4740dad5ee 7fe0b0251b
Коммит 506982a7d0
108 изменённых файлов: 3067 добавлений и 1645 удалений

Просмотреть файл

@ -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.

Двоичные данные
browser/themes/linux/devtools/responsive-horizontal-resizer.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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)

Двоичные данные
browser/themes/osx/devtools/responsive-horizontal-resizer.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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;

Двоичные данные
browser/themes/windows/devtools/responsive-horizontal-resizer.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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 {

Двоичные данные
mobile/android/themes/core/images/amo-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 653 B

Двоичные данные
mobile/android/themes/core/images/chevron.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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>

Просмотреть файл

@ -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();

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше