mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge m-c to b2g-inbound.
This commit is contained in:
commit
09f87e0587
@ -14,7 +14,7 @@
|
||||
"{objdir}/dist/b2g-*.crashreporter-symbols.zip",
|
||||
"{objdir}/dist/b2g-*.tar.gz",
|
||||
"{workdir}/sources.xml",
|
||||
"{workdir}/out/target/product/hamachi/fota-update.mar"
|
||||
"{workdir}/out/target/product/hamachi/*.mar"
|
||||
],
|
||||
"env": {
|
||||
"VARIANT": "user",
|
||||
|
@ -388,7 +388,7 @@
|
||||
#endif
|
||||
@BINPATH@/components/SiteSpecificUserAgent.js
|
||||
@BINPATH@/components/SiteSpecificUserAgent.manifest
|
||||
@BINPATH@/components/storage-mozStorage.js
|
||||
@BINPATH@/components/storage-json.js
|
||||
@BINPATH@/components/crypto-SDR.js
|
||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||
@BINPATH@/components/jsconsole-clhandler.js
|
||||
|
@ -1129,6 +1129,18 @@ var gBrowserInit = {
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// Load the Login Manager data from disk off the main thread, some time
|
||||
// after startup. If the data is required before the timeout, for example
|
||||
// because a restored page contains a password field, it will be loaded on
|
||||
// the main thread, and this initialization request will be ignored.
|
||||
setTimeout(function() {
|
||||
try {
|
||||
Services.logins;
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// The object handling the downloads indicator is also initialized here in the
|
||||
// delayed startup function, but the actual indicator element is not loaded
|
||||
// unless there are downloads to be displayed.
|
||||
|
@ -328,6 +328,9 @@ const PanelUI = {
|
||||
tempPanel.setAttribute("type", "arrow");
|
||||
tempPanel.setAttribute("id", "customizationui-widget-panel");
|
||||
tempPanel.setAttribute("class", "cui-widget-panel");
|
||||
if (this._disableAnimations) {
|
||||
tempPanel.setAttribute("animate", "false");
|
||||
}
|
||||
tempPanel.setAttribute("context", "");
|
||||
document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
|
||||
// If the view has a footer, set a convenience class on the panel.
|
||||
@ -363,6 +366,19 @@ const PanelUI = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* NB: The enable- and disableSingleSubviewPanelAnimations methods only
|
||||
* affect the hiding/showing animations of single-subview panels (tempPanel
|
||||
* in the showSubView method).
|
||||
*/
|
||||
disableSingleSubviewPanelAnimations: function() {
|
||||
this._disableAnimations = true;
|
||||
},
|
||||
|
||||
enableSingleSubviewPanelAnimations: function() {
|
||||
this._disableAnimations = false;
|
||||
},
|
||||
|
||||
onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) {
|
||||
if (aContainer != this.contents) {
|
||||
return;
|
||||
|
@ -41,6 +41,16 @@ function createSidebarItem() {
|
||||
gSidebarMenu.insertBefore(gTestSidebarItem, gSidebarMenu.firstChild);
|
||||
}
|
||||
|
||||
function addWidget() {
|
||||
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
|
||||
PanelUI.disableSingleSubviewPanelAnimations();
|
||||
}
|
||||
|
||||
function removeWidget() {
|
||||
CustomizableUI.removeWidgetFromArea("sidebar-button");
|
||||
PanelUI.enableSingleSubviewPanelAnimations();
|
||||
}
|
||||
|
||||
// Filters out the trailing menuseparators from the sidebar list
|
||||
function getSidebarList() {
|
||||
let sidebars = [...gSidebarMenu.children];
|
||||
@ -76,7 +86,7 @@ let showSidebarPopup = Task.async(function*() {
|
||||
|
||||
// Check the sidebar widget shows the default items
|
||||
add_task(function*() {
|
||||
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
|
||||
addWidget();
|
||||
|
||||
yield showSidebarPopup();
|
||||
|
||||
@ -89,13 +99,14 @@ add_task(function*() {
|
||||
document.getElementById("customizationui-widget-panel").hidePopup();
|
||||
yield subviewHiddenPromise;
|
||||
|
||||
return resetCustomization();
|
||||
removeWidget();
|
||||
});
|
||||
|
||||
function add_sidebar_task(description, setup, teardown) {
|
||||
add_task(function*() {
|
||||
info(description);
|
||||
createSidebarItem();
|
||||
addWidget();
|
||||
yield setup();
|
||||
|
||||
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
|
||||
@ -114,7 +125,7 @@ function add_sidebar_task(description, setup, teardown) {
|
||||
|
||||
yield teardown();
|
||||
gTestSidebarItem.remove();
|
||||
return resetCustomization();
|
||||
removeWidget();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -105,13 +105,13 @@ const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
|
||||
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
|
||||
|
||||
// Seconds of idle before trying to create a bookmarks backup.
|
||||
const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
|
||||
const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
|
||||
// Minimum interval between backups. We try to not create more than one backup
|
||||
// per interval.
|
||||
const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
|
||||
// Maximum interval between backups. If the last backup is older than these
|
||||
// days we will try to create a new one more aggressively.
|
||||
const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 5;
|
||||
const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 3;
|
||||
|
||||
// Factory object
|
||||
const BrowserGlueServiceFactory = {
|
||||
|
@ -27,7 +27,7 @@ function test() {
|
||||
|
||||
// Check only valid tabs are shown
|
||||
let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
|
||||
let expectedTabs = ["options", "webconsole", "jsdebugger", "scratchpad"];
|
||||
let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"];
|
||||
|
||||
is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
|
||||
Array.forEach(tabs, (tab, i) => {
|
||||
|
@ -58,7 +58,7 @@ function testSelectTool(aToolbox) {
|
||||
|
||||
function testPreferenceAndUIStateIsConsistent() {
|
||||
let checkNodes = [...panelWin.document.querySelectorAll("#enabled-toolbox-buttons-box > checkbox")];
|
||||
let toolboxButtonNodes = [...doc.querySelectorAll("#toolbox-buttons > toolbarbutton")];
|
||||
let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
|
||||
let toggleableTools = toolbox.toolboxButtons;
|
||||
|
||||
for (let tool of toggleableTools) {
|
||||
@ -74,7 +74,7 @@ function testPreferenceAndUIStateIsConsistent() {
|
||||
|
||||
function testToggleToolboxButtons() {
|
||||
let checkNodes = [...panelWin.document.querySelectorAll("#enabled-toolbox-buttons-box > checkbox")];
|
||||
let toolboxButtonNodes = [...doc.querySelectorAll("#toolbox-buttons > toolbarbutton")];
|
||||
let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
|
||||
let visibleButtons = toolboxButtonNodes.filter(button=>!button.hasAttribute("hidden"));
|
||||
let toggleableTools = toolbox.toolboxButtons;
|
||||
|
||||
|
@ -555,7 +555,7 @@ Toolbox.prototype = {
|
||||
this._pickerButton.className = "command-button command-button-invertable";
|
||||
this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip"));
|
||||
|
||||
let container = this.doc.querySelector("#toolbox-buttons");
|
||||
let container = this.doc.querySelector("#toolbox-picker-container");
|
||||
container.appendChild(this._pickerButton);
|
||||
|
||||
this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
|
||||
@ -690,21 +690,29 @@ Toolbox.prototype = {
|
||||
vbox.id = "toolbox-panel-" + id;
|
||||
}
|
||||
|
||||
// If there is no tab yet, or the ordinal to be added is the largest one.
|
||||
if (tabs.childNodes.length == 0 ||
|
||||
+tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
|
||||
tabs.appendChild(radio);
|
||||
if (id === "options") {
|
||||
// Options panel is special. It doesn't belong in the same container as
|
||||
// the other tabs.
|
||||
let optionTabContainer = this.doc.getElementById("toolbox-option-container");
|
||||
optionTabContainer.appendChild(radio);
|
||||
deck.appendChild(vbox);
|
||||
} else {
|
||||
// else, iterate over all the tabs to get the correct location.
|
||||
Array.some(tabs.childNodes, (node, i) => {
|
||||
if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
|
||||
tabs.insertBefore(radio, node);
|
||||
deck.insertBefore(vbox, deck.childNodes[i]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// If there is no tab yet, or the ordinal to be added is the largest one.
|
||||
if (tabs.childNodes.length == 0 ||
|
||||
tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
|
||||
tabs.appendChild(radio);
|
||||
deck.appendChild(vbox);
|
||||
} else {
|
||||
// else, iterate over all the tabs to get the correct location.
|
||||
Array.some(tabs.childNodes, (node, i) => {
|
||||
if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
|
||||
tabs.insertBefore(radio, node);
|
||||
deck.insertBefore(vbox, deck.childNodes[i]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._addKeysToWindow();
|
||||
@ -805,6 +813,15 @@ Toolbox.prototype = {
|
||||
let tab = this.doc.getElementById("toolbox-tab-" + id);
|
||||
tab.setAttribute("selected", "true");
|
||||
|
||||
// If options is selected, the separator between it and the
|
||||
// command buttons should be hidden.
|
||||
let sep = this.doc.getElementById("toolbox-controls-separator");
|
||||
if (id === "options") {
|
||||
sep.setAttribute("invisible", "true");
|
||||
} else {
|
||||
sep.removeAttribute("invisible");
|
||||
}
|
||||
|
||||
if (this.currentToolId == id) {
|
||||
// re-focus tool to get key events again
|
||||
this.focusTool(id);
|
||||
@ -831,20 +848,13 @@ Toolbox.prototype = {
|
||||
let tabstrip = this.doc.getElementById("toolbox-tabs");
|
||||
|
||||
// select the right tab, making 0th index the default tab if right tab not
|
||||
// found
|
||||
let index = 0;
|
||||
let tabs = tabstrip.childNodes;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
if (tabs[i] === tab) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tabstrip.selectedItem = tab;
|
||||
// found.
|
||||
tabstrip.selectedItem = tab || tabstrip.childNodes[0];
|
||||
|
||||
// and select the right iframe
|
||||
let deck = this.doc.getElementById("toolbox-deck");
|
||||
deck.selectedIndex = index;
|
||||
let panel = this.doc.getElementById("toolbox-panel-" + id);
|
||||
deck.selectedPanel = panel;
|
||||
|
||||
this.currentToolId = id;
|
||||
this._refreshConsoleDisplay();
|
||||
@ -907,8 +917,10 @@ Toolbox.prototype = {
|
||||
* Loads the tool next to the currently selected tool.
|
||||
*/
|
||||
selectNextTool: function() {
|
||||
let tools = this.doc.querySelectorAll(".devtools-tab");
|
||||
let selected = this.doc.querySelector(".devtools-tab[selected]");
|
||||
let next = selected.nextSibling || selected.parentNode.firstChild;
|
||||
let nextIndex = [...tools].indexOf(selected) + 1;
|
||||
let next = tools[nextIndex] || tools[0];
|
||||
let tool = next.getAttribute("toolid");
|
||||
return this.selectTool(tool);
|
||||
},
|
||||
@ -917,9 +929,11 @@ Toolbox.prototype = {
|
||||
* Loads the tool just left to the currently selected tool.
|
||||
*/
|
||||
selectPreviousTool: function() {
|
||||
let tools = this.doc.querySelectorAll(".devtools-tab");
|
||||
let selected = this.doc.querySelector(".devtools-tab[selected]");
|
||||
let previous = selected.previousSibling || selected.parentNode.lastChild;
|
||||
let tool = previous.getAttribute("toolid");
|
||||
let prevIndex = [...tools].indexOf(selected) - 1;
|
||||
let prev = tools[prevIndex] || tools[tools.length - 1];
|
||||
let tool = prev.getAttribute("toolid");
|
||||
return this.selectTool(tool);
|
||||
},
|
||||
|
||||
|
@ -51,26 +51,17 @@
|
||||
|
||||
<notificationbox id="toolbox-notificationbox" flex="1">
|
||||
<toolbar class="devtools-tabbar">
|
||||
#ifdef XP_MACOSX
|
||||
<hbox id="toolbox-controls">
|
||||
<toolbarbutton id="toolbox-close"
|
||||
class="devtools-closebutton"
|
||||
tooltiptext="&toolboxCloseButton.tooltip;"/>
|
||||
<hbox id="toolbox-dock-buttons"/>
|
||||
</hbox>
|
||||
#endif
|
||||
<hbox id="toolbox-tabs" flex="1">
|
||||
</hbox>
|
||||
<hbox id="toolbox-picker-container" />
|
||||
<hbox id="toolbox-tabs" flex="1" />
|
||||
<hbox id="toolbox-buttons" pack="end"/>
|
||||
#ifndef XP_MACOSX
|
||||
<vbox id="toolbox-controls-separator"/>
|
||||
<hbox id="toolbox-option-container"/>
|
||||
<hbox id="toolbox-controls">
|
||||
<hbox id="toolbox-dock-buttons"/>
|
||||
<toolbarbutton id="toolbox-close"
|
||||
class="devtools-closebutton"
|
||||
tooltiptext="&toolboxCloseButton.tooltip;"/>
|
||||
</hbox>
|
||||
#endif
|
||||
</toolbar>
|
||||
<vbox flex="1">
|
||||
<deck id="toolbox-deck" flex="1" minheight="75" />
|
||||
|
@ -86,7 +86,7 @@ Tools.webConsole = {
|
||||
key: l10n("cmd.commandkey", webConsoleStrings),
|
||||
accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
|
||||
modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
|
||||
ordinal: 1,
|
||||
ordinal: 2,
|
||||
icon: "chrome://browser/skin/devtools/tool-webconsole.svg",
|
||||
invertIconForLightTheme: true,
|
||||
url: "chrome://browser/content/devtools/webconsole.xul",
|
||||
@ -117,7 +117,7 @@ Tools.inspector = {
|
||||
id: "inspector",
|
||||
accesskey: l10n("inspector.accesskey", inspectorStrings),
|
||||
key: l10n("inspector.commandkey", inspectorStrings),
|
||||
ordinal: 2,
|
||||
ordinal: 1,
|
||||
modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
|
||||
icon: "chrome://browser/skin/devtools/tool-inspector.svg",
|
||||
invertIconForLightTheme: true,
|
||||
|
@ -159,7 +159,7 @@
|
||||
</div>
|
||||
<div class="csscoverage-report-unused">
|
||||
<h2>&csscoverage.unused;</h2>
|
||||
<p>&csscoverage.noMatch;</p>
|
||||
<p>&csscoverage.noMatches;</p>
|
||||
<div foreach="page in ${unused}">
|
||||
<h3>${page.url}</h3>
|
||||
<code foreach="rule in ${page.rules}"
|
||||
|
@ -390,7 +390,7 @@
|
||||
@BINPATH@/components/nsLoginInfo.js
|
||||
@BINPATH@/components/nsLoginManager.js
|
||||
@BINPATH@/components/nsLoginManagerPrompter.js
|
||||
@BINPATH@/components/storage-mozStorage.js
|
||||
@BINPATH@/components/storage-json.js
|
||||
@BINPATH@/components/crypto-SDR.js
|
||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||
@BINPATH@/components/jsconsole-clhandler.js
|
||||
|
@ -278,7 +278,7 @@
|
||||
margin: 0 4px;
|
||||
min-width: 16px;
|
||||
width: 16px;
|
||||
opacity: 0.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.devtools-closebutton > image {
|
||||
@ -302,10 +302,6 @@
|
||||
}
|
||||
|
||||
.devtools-closebutton:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.devtools-closebutton:hover:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -457,10 +453,6 @@
|
||||
* Rules that apply to the global toolbox like command buttons,
|
||||
* devtools tabs, docking buttons, etc. */
|
||||
|
||||
#toolbox-controls {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
#toolbox-controls > toolbarbutton,
|
||||
#toolbox-dock-buttons > toolbarbutton {
|
||||
-moz-appearance: none;
|
||||
@ -500,7 +492,7 @@
|
||||
#toolbox-dock-window,
|
||||
#toolbox-dock-bottom,
|
||||
#toolbox-dock-side {
|
||||
opacity: 0.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#toolbox-dock-window:hover,
|
||||
@ -510,14 +502,16 @@
|
||||
}
|
||||
|
||||
#toolbox-controls-separator {
|
||||
width: 3px;
|
||||
width: 2px;
|
||||
background-image: linear-gradient(hsla(204,45%,98%,0), hsla(204,45%,98%,.1), hsla(204,45%,98%,0)),
|
||||
linear-gradient(hsla(206,37%,4%,0), hsla(206,37%,4%,.6), hsla(206,37%,4%,0)),
|
||||
linear-gradient(hsla(204,45%,98%,0), hsla(204,45%,98%,.1), hsla(204,45%,98%,0));
|
||||
background-size: 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0, 1px, 2px;
|
||||
-moz-margin-start: 8px;
|
||||
}
|
||||
#toolbox-controls-separator[invisible] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Command buttons */
|
||||
@ -650,7 +644,9 @@
|
||||
max-width: 127px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-border-start: 1px solid;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
-moz-border-start-width: 1px;
|
||||
-moz-box-align: center;
|
||||
}
|
||||
|
||||
@ -691,18 +687,6 @@
|
||||
background-color: rgba(44, 187, 15, .2);
|
||||
}
|
||||
|
||||
.devtools-tab:first-child {
|
||||
-moz-border-start-width: 0;
|
||||
}
|
||||
|
||||
.theme-light .devtools-tab:last-child {
|
||||
-moz-border-end: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.theme-dark .devtools-tab:last-child {
|
||||
-moz-border-end: 1px solid #42484f;
|
||||
}
|
||||
|
||||
.devtools-tab > image {
|
||||
border: none;
|
||||
-moz-margin-end: 0;
|
||||
@ -712,10 +696,6 @@
|
||||
width: 16px; /* Prevents collapse during theme switching */
|
||||
}
|
||||
|
||||
#toolbox-tab-options > image {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.devtools-tab > label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -729,7 +709,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-dark #toolbox-tabs .devtools-tab[selected] {
|
||||
.theme-dark .devtools-tab[selected] {
|
||||
color: #f5f7fa;
|
||||
background-color: #1a4666;
|
||||
box-shadow: 0 2px 0 #d7f1ff inset,
|
||||
@ -737,7 +717,7 @@
|
||||
0 -2px 0 rgba(0,0,0,.2) inset;
|
||||
}
|
||||
|
||||
.theme-light #toolbox-tabs .devtools-tab[selected] {
|
||||
.theme-light .devtools-tab[selected] {
|
||||
color: #f5f7fa;
|
||||
background-color: #4c9ed9;
|
||||
box-shadow: 0 2px 0 #d7f1ff inset,
|
||||
@ -745,29 +725,51 @@
|
||||
0 -2px 0 rgba(0,0,0,.06) inset;
|
||||
}
|
||||
|
||||
.devtools-tab[selected]:not(:first-child),
|
||||
.devtools-tab[highlighted]:not(:first-child) {
|
||||
#toolbox-tabs .devtools-tab[selected]:not(:first-child),
|
||||
#toolbox-tabs .devtools-tab[highlighted]:not(:first-child) {
|
||||
border-width: 0;
|
||||
-moz-padding-start: 1px;
|
||||
}
|
||||
|
||||
.devtools-tab[selected]:last-child,
|
||||
.devtools-tab[highlighted]:last-child {
|
||||
#toolbox-tabs .devtools-tab[selected]:last-child,
|
||||
#toolbox-tabs .devtools-tab[highlighted]:last-child {
|
||||
-moz-padding-end: 1px;
|
||||
}
|
||||
|
||||
.devtools-tab[selected] + .devtools-tab,
|
||||
.devtools-tab[highlighted] + .devtools-tab {
|
||||
#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
|
||||
#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
|
||||
-moz-border-start-width: 0;
|
||||
-moz-padding-start: 1px;
|
||||
}
|
||||
|
||||
#toolbox-tabs .devtools-tab:first-child[selected] {
|
||||
-moz-border-start-width: 0;
|
||||
}
|
||||
|
||||
#toolbox-tabs .devtools-tab:last-child {
|
||||
-moz-border-end-width: 1px;
|
||||
}
|
||||
|
||||
.devtools-tab:not([highlighted]) > .highlighted-icon,
|
||||
.devtools-tab[selected] > .highlighted-icon,
|
||||
.devtools-tab:not([selected])[highlighted] > .default-icon {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
/* The options tab is special - it doesn't have the same parent
|
||||
as the other tabs (toolbox-option-container vs toolbox-tabs) */
|
||||
#toolbox-option-container .devtools-tab:not([selected]) {
|
||||
background-color: transparent;
|
||||
}
|
||||
#toolbox-option-container .devtools-tab {
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
-moz-padding-start: 1px;
|
||||
}
|
||||
#toolbox-tab-options > image {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* Invert the colors of certain dark theme images for displaying
|
||||
* inside of the light theme.
|
||||
*/
|
||||
|
@ -100,3 +100,12 @@
|
||||
fun:_ZN22nsComponentManagerImpl17ManifestComponentERNS_25ManifestProcessingContextEiPKPc
|
||||
...
|
||||
}
|
||||
{
|
||||
Bug 1017112
|
||||
Memcheck:Leak
|
||||
fun:malloc
|
||||
...
|
||||
fun:PK11_InitPin
|
||||
fun:_ZN11nsPK11Token12InitPasswordEPKDs
|
||||
...
|
||||
}
|
||||
|
@ -152,7 +152,10 @@ AudioStream::AudioStream()
|
||||
, mAudioClock(MOZ_THIS_IN_INITIALIZER_LIST())
|
||||
, mLatencyRequest(HighLatency)
|
||||
, mReadPoint(0)
|
||||
, mLostFrames(0)
|
||||
, mWrittenFramesPast(0)
|
||||
, mLostFramesPast(0)
|
||||
, mWrittenFramesLast(0)
|
||||
, mLostFramesLast(0)
|
||||
, mDumpFile(nullptr)
|
||||
, mVolume(1.0)
|
||||
, mBytesPerFrame(0)
|
||||
@ -780,9 +783,28 @@ AudioStream::GetPositionInFramesUnlocked()
|
||||
|
||||
// Adjust the reported position by the number of silent frames written
|
||||
// during stream underruns.
|
||||
// Since frames sent to DataCallback is not consumed by the backend immediately,
|
||||
// it will be an over adjustment if we return |position - mLostFramesPast - mLostFramesLast|.
|
||||
// On the other hand, we need to keep the whole history of frames sent to DataCallback
|
||||
// in order to adjust position correctly which will require more storage.
|
||||
// We choose a simple way to store the history where |mWrittenFramesPast| and
|
||||
// |mLostFramesPast| are the sum of frames from 1th to |N-1|th callbacks, and
|
||||
// |mWrittenFramesLast| and |mLostFramesLast| represent the frames sent in last callback.
|
||||
// When |position| lies in
|
||||
// [mWrittenFramesPast+mLostFramesPast, mWrittenFramesPast+mLostFramesPast+mWrittenFramesLast+mLostFramesLast],
|
||||
// we will be able to adjust position precisely which should be the major case.
|
||||
// If |position| falls in [0, mWrittenFramesPast+mLostFramesPast), there will be an
|
||||
// error in the adjustment. However that is fine as long as we can ensure the
|
||||
// adjusted position is mono-increasing to avoid audio clock going backward.
|
||||
uint64_t adjustedPosition = 0;
|
||||
if (position >= mLostFrames) {
|
||||
adjustedPosition = position - mLostFrames;
|
||||
if (position <= mWrittenFramesPast) {
|
||||
adjustedPosition = position;
|
||||
} else if (position <= mWrittenFramesPast + mLostFramesPast) {
|
||||
adjustedPosition = mWrittenFramesPast;
|
||||
} else if (position <= mWrittenFramesPast + mLostFramesPast + mWrittenFramesLast) {
|
||||
adjustedPosition = position - mLostFramesPast;
|
||||
} else {
|
||||
adjustedPosition = mWrittenFramesPast + mWrittenFramesLast;
|
||||
}
|
||||
return std::min<uint64_t>(adjustedPosition, INT64_MAX);
|
||||
}
|
||||
@ -929,6 +951,9 @@ AudioStream::DataCallback(void* aBuffer, long aFrames)
|
||||
uint32_t servicedFrames = 0;
|
||||
int64_t insertTime;
|
||||
|
||||
mWrittenFramesPast += mWrittenFramesLast;
|
||||
mLostFramesPast += mLostFramesLast;
|
||||
|
||||
// NOTE: wasapi (others?) can call us back *after* stop()/Shutdown() (mState == SHUTDOWN)
|
||||
// Bug 996162
|
||||
|
||||
@ -992,6 +1017,8 @@ AudioStream::DataCallback(void* aBuffer, long aFrames)
|
||||
}
|
||||
|
||||
underrunFrames = aFrames - servicedFrames;
|
||||
mWrittenFramesLast = servicedFrames;
|
||||
mLostFramesLast = underrunFrames;
|
||||
|
||||
if (mState != DRAINING) {
|
||||
uint8_t* rpos = static_cast<uint8_t*>(aBuffer) + FramesToBytes(aFrames - underrunFrames);
|
||||
@ -1000,7 +1027,6 @@ AudioStream::DataCallback(void* aBuffer, long aFrames)
|
||||
PR_LOG(gAudioStreamLog, PR_LOG_WARNING,
|
||||
("AudioStream %p lost %d frames", this, underrunFrames));
|
||||
}
|
||||
mLostFrames += underrunFrames;
|
||||
servicedFrames += underrunFrames;
|
||||
}
|
||||
|
||||
|
@ -65,8 +65,6 @@ public:
|
||||
// Get the current pitch preservation state.
|
||||
// Called on the audio thread.
|
||||
bool GetPreservesPitch();
|
||||
// Get the number of frames written to the backend.
|
||||
int64_t GetWritten();
|
||||
private:
|
||||
// This AudioStream holds a strong reference to this AudioClock. This
|
||||
// pointer is garanteed to always be valid.
|
||||
@ -372,9 +370,14 @@ private:
|
||||
};
|
||||
nsAutoTArray<Inserts, 8> mInserts;
|
||||
|
||||
// Sum of silent frames written when DataCallback requests more frames
|
||||
// than are available in mBuffer.
|
||||
uint64_t mLostFrames;
|
||||
// Suppose we have received DataCallback for N times, |mWrittenFramesPast|
|
||||
// and |mLostFramesPast| are the sum of frames written to the backend from
|
||||
// 1st to |N-1|th DataCallbacks.
|
||||
uint64_t mWrittenFramesPast; // non-silent frames
|
||||
uint64_t mLostFramesPast; // silent frames
|
||||
// Frames written to the backend in Nth DataCallback.
|
||||
uint64_t mWrittenFramesLast; // non-silent frames
|
||||
uint64_t mLostFramesLast; // silent frames
|
||||
|
||||
// Output file for dumping audio
|
||||
FILE* mDumpFile;
|
||||
|
@ -140,7 +140,11 @@ SVGDocument::EnsureNonSVGUserAgentStyleSheetsLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
EnsureOnDemandBuiltInUASheet(nsLayoutStylesheetCache::NumberControlSheet());
|
||||
nsCSSStyleSheet* sheet = nsLayoutStylesheetCache::NumberControlSheet();
|
||||
if (sheet) {
|
||||
// number-control.css can be behind a pref
|
||||
EnsureOnDemandBuiltInUASheet(sheet);
|
||||
}
|
||||
EnsureOnDemandBuiltInUASheet(nsLayoutStylesheetCache::FormsSheet());
|
||||
EnsureOnDemandBuiltInUASheet(nsLayoutStylesheetCache::HTMLSheet());
|
||||
EnsureOnDemandBuiltInUASheet(nsLayoutStylesheetCache::UASheet());
|
||||
|
@ -138,14 +138,21 @@ struct AlignedArray
|
||||
T *mPtr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns aStride increased, if necessary, so that it divides exactly into
|
||||
* |alignment|.
|
||||
*
|
||||
* Note that currently |alignment| must be a power-of-2. If for some reason we
|
||||
* want to support NPOT alignment we can revert back to this functions old
|
||||
* implementation.
|
||||
*/
|
||||
template<int alignment>
|
||||
int32_t GetAlignedStride(int32_t aStride)
|
||||
{
|
||||
if (aStride % alignment) {
|
||||
return aStride + (alignment - (aStride % alignment));
|
||||
}
|
||||
|
||||
return aStride;
|
||||
static_assert(alignment > 0 && (alignment & (alignment-1)) == 0,
|
||||
"This implementation currently require power-of-two alignment");
|
||||
const int32_t mask = alignment - 1;
|
||||
return (aStride + mask) & ~mask;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -106,11 +106,25 @@ ImageHost::Composite(EffectChain& aEffectChain,
|
||||
gfx::Rect pictureRect(0, 0,
|
||||
mPictureRect.width,
|
||||
mPictureRect.height);
|
||||
//XXX: We might have multiple texture sources here (e.g. 3 YCbCr textures), and we're
|
||||
// only iterating over the tiles of the first one. Are we assuming that the tiling
|
||||
// will be identical? Can we ensure that somehow?
|
||||
BigImageIterator* it = source->AsBigImageIterator();
|
||||
if (it) {
|
||||
|
||||
// This iteration does not work if we have multiple texture sources here
|
||||
// (e.g. 3 YCbCr textures). There's nothing preventing the different
|
||||
// planes from having different resolutions or tile sizes. For example, a
|
||||
// YCbCr frame could have Cb and Cr planes that are half the resolution of
|
||||
// the Y plane, in such a way that the Y plane overflows the maximum
|
||||
// texture size and the Cb and Cr planes do not. Then the Y plane would be
|
||||
// split into multiple tiles and the Cb and Cr planes would just be one
|
||||
// tile each.
|
||||
// To handle the general case correctly, we'd have to create a grid of
|
||||
// intersected tiles over all planes, and then draw each grid tile using
|
||||
// the corresponding source tiles from all planes, with appropriate
|
||||
// per-plane per-tile texture coords.
|
||||
// DrawQuad currently assumes that all planes use the same texture coords.
|
||||
MOZ_ASSERT(it->GetTileCount() == 1 || !source->GetNextSibling(),
|
||||
"Can't handle multi-plane BigImages");
|
||||
|
||||
it->BeginBigImageIteration();
|
||||
do {
|
||||
nsIntRect tileRect = it->GetTileRect();
|
||||
|
@ -307,11 +307,24 @@ CompositorOGL::Initialize()
|
||||
mGLContext->fGenBuffers(1, &mQuadVBO);
|
||||
mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO);
|
||||
|
||||
// 4 quads, with the number of the quad (vertexID) encoded in w.
|
||||
GLfloat vertices[] = {
|
||||
/* First quad vertices */
|
||||
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
|
||||
/* Then quad texcoords */
|
||||
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
1.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.0f, 0.0f,
|
||||
1.0f, 1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.0f, 1.0f, 0.0f, 1.0f,
|
||||
1.0f, 1.0f, 0.0f, 1.0f,
|
||||
0.0f, 0.0f, 0.0f, 2.0f,
|
||||
1.0f, 0.0f, 0.0f, 2.0f,
|
||||
0.0f, 1.0f, 0.0f, 2.0f,
|
||||
1.0f, 1.0f, 0.0f, 2.0f,
|
||||
0.0f, 0.0f, 0.0f, 3.0f,
|
||||
1.0f, 0.0f, 0.0f, 3.0f,
|
||||
0.0f, 1.0f, 0.0f, 3.0f,
|
||||
1.0f, 1.0f, 0.0f, 3.0f,
|
||||
};
|
||||
HeapCopyOfStackArray<GLfloat> verticesOnHeap(vertices);
|
||||
mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER,
|
||||
@ -528,9 +541,7 @@ CompositorOGL::BindAndDrawQuadWithTextureRect(ShaderProgramOGL *aProg,
|
||||
aTexCoordRect,
|
||||
layerRects,
|
||||
textureRects);
|
||||
for (int n = 0; n < rects; ++n) {
|
||||
BindAndDrawQuad(aProg, layerRects[n], textureRects[n]);
|
||||
}
|
||||
BindAndDrawQuads(aProg, rects, layerRects, textureRects);
|
||||
}
|
||||
|
||||
void
|
||||
@ -1499,26 +1510,27 @@ CompositorOGL::BindQuadVBO() {
|
||||
|
||||
void
|
||||
CompositorOGL::QuadVBOVerticesAttrib(GLuint aAttribIndex) {
|
||||
mGLContext->fVertexAttribPointer(aAttribIndex, 2,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*) QuadVBOVertexOffset());
|
||||
mGLContext->fVertexAttribPointer(aAttribIndex, 4,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*) 0);
|
||||
}
|
||||
|
||||
void
|
||||
CompositorOGL::QuadVBOTexCoordsAttrib(GLuint aAttribIndex) {
|
||||
mGLContext->fVertexAttribPointer(aAttribIndex, 2,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*) QuadVBOTexCoordOffset());
|
||||
mGLContext->fVertexAttribPointer(aAttribIndex, 4,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*) 0);
|
||||
}
|
||||
|
||||
void
|
||||
CompositorOGL::BindAndDrawQuad(ShaderProgramOGL *aProg,
|
||||
const Rect& aLayerRect,
|
||||
const Rect& aTextureRect)
|
||||
CompositorOGL::BindAndDrawQuads(ShaderProgramOGL *aProg,
|
||||
int aQuads,
|
||||
const Rect* aLayerRects,
|
||||
const Rect* aTextureRects)
|
||||
{
|
||||
NS_ASSERTION(aProg->HasInitialized(), "Shader program not correctly initialized");
|
||||
|
||||
aProg->SetLayerRect(aLayerRect);
|
||||
aProg->SetLayerRects(aLayerRects);
|
||||
|
||||
GLuint vertAttribIndex = aProg->AttribLocation(ShaderProgramOGL::VertexCoordAttrib);
|
||||
GLuint texCoordAttribIndex = aProg->AttribLocation(ShaderProgramOGL::TexCoordAttrib);
|
||||
@ -1530,11 +1542,11 @@ CompositorOGL::BindAndDrawQuad(ShaderProgramOGL *aProg,
|
||||
QuadVBOTexCoordsAttrib(texCoordAttribIndex);
|
||||
mGLContext->fEnableVertexAttribArray(texCoordAttribIndex);
|
||||
|
||||
aProg->SetTextureRect(aTextureRect);
|
||||
aProg->SetTextureRects(aTextureRects);
|
||||
}
|
||||
|
||||
mGLContext->fEnableVertexAttribArray(vertAttribIndex);
|
||||
mGLContext->fDrawArrays(LOCAL_GL_TRIANGLE_STRIP, 0, 4);
|
||||
mGLContext->fDrawArrays(LOCAL_GL_TRIANGLE_STRIP, 0, 4 * aQuads);
|
||||
}
|
||||
|
||||
GLuint
|
||||
|
@ -352,15 +352,22 @@ private:
|
||||
GLuint aSourceFrameBuffer,
|
||||
GLuint *aFBO, GLuint *aTexture);
|
||||
|
||||
GLintptr QuadVBOVertexOffset() { return 0; }
|
||||
GLintptr QuadVBOTexCoordOffset() { return sizeof(float)*4*2; }
|
||||
|
||||
void BindQuadVBO();
|
||||
void QuadVBOVerticesAttrib(GLuint aAttribIndex);
|
||||
void QuadVBOTexCoordsAttrib(GLuint aAttribIndex);
|
||||
void BindAndDrawQuads(ShaderProgramOGL *aProg,
|
||||
int aQuads,
|
||||
const gfx::Rect* aLayerRect,
|
||||
const gfx::Rect* aTextureRect);
|
||||
void BindAndDrawQuad(ShaderProgramOGL *aProg,
|
||||
const gfx::Rect& aLayerRect,
|
||||
const gfx::Rect& aTextureRect = gfx::Rect(0.0f, 0.0f, 1.0f, 1.0f));
|
||||
const gfx::Rect& aTextureRect = gfx::Rect(0.0f, 0.0f, 1.0f, 1.0f)) {
|
||||
gfx::Rect layerRects[4];
|
||||
gfx::Rect textureRects[4];
|
||||
layerRects[0] = aLayerRect;
|
||||
textureRects[0] = aTextureRect;
|
||||
BindAndDrawQuads(aProg, 1, layerRects, textureRects);
|
||||
}
|
||||
void BindAndDrawQuadWithTextureRect(ShaderProgramOGL *aProg,
|
||||
const gfx::Rect& aRect,
|
||||
const gfx::Rect& aTexCoordRect,
|
||||
|
@ -32,10 +32,10 @@ AddUniforms(ProgramProfileOGL& aProfile)
|
||||
static const char *sKnownUniformNames[] = {
|
||||
"uLayerTransform",
|
||||
"uMaskTransform",
|
||||
"uLayerRect",
|
||||
"uLayerRects",
|
||||
"uMatrixProj",
|
||||
"uTextureTransform",
|
||||
"uTextureRect",
|
||||
"uTextureRects",
|
||||
"uRenderTargetOffset",
|
||||
"uLayerOpacity",
|
||||
"uTexture",
|
||||
@ -146,7 +146,7 @@ ProgramProfileOGL::GetProfileFor(ShaderConfigOGL aConfig)
|
||||
AddUniforms(result);
|
||||
|
||||
vs << "uniform mat4 uMatrixProj;" << endl;
|
||||
vs << "uniform vec4 uLayerRect;" << endl;
|
||||
vs << "uniform vec4 uLayerRects[4];" << endl;
|
||||
vs << "uniform mat4 uLayerTransform;" << endl;
|
||||
vs << "uniform vec4 uRenderTargetOffset;" << endl;
|
||||
|
||||
@ -154,8 +154,8 @@ ProgramProfileOGL::GetProfileFor(ShaderConfigOGL aConfig)
|
||||
|
||||
if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
|
||||
vs << "uniform mat4 uTextureTransform;" << endl;
|
||||
vs << "uniform vec4 uTextureRect;" << endl;
|
||||
vs << "attribute vec2 aTexCoord;" << endl;
|
||||
vs << "uniform vec4 uTextureRects[4];" << endl;
|
||||
vs << "attribute vec4 aTexCoord;" << endl;
|
||||
vs << "varying vec2 vTexCoord;" << endl;
|
||||
}
|
||||
|
||||
@ -166,7 +166,9 @@ ProgramProfileOGL::GetProfileFor(ShaderConfigOGL aConfig)
|
||||
}
|
||||
|
||||
vs << "void main() {" << endl;
|
||||
vs << " vec4 finalPosition = vec4(aVertexCoord.xy * uLayerRect.zw + uLayerRect.xy, 0.0, 1.0);" << endl;
|
||||
vs << " int vertexID = int(aVertexCoord.w);" << endl;
|
||||
vs << " vec4 layerRect = uLayerRects[vertexID];" << endl;
|
||||
vs << " vec4 finalPosition = vec4(aVertexCoord.xy * layerRect.zw + layerRect.xy, 0.0, 1.0);" << endl;
|
||||
vs << " finalPosition = uLayerTransform * finalPosition;" << endl;
|
||||
vs << " finalPosition.xyz /= finalPosition.w;" << endl;
|
||||
|
||||
@ -184,7 +186,8 @@ ProgramProfileOGL::GetProfileFor(ShaderConfigOGL aConfig)
|
||||
vs << " finalPosition = uMatrixProj * finalPosition;" << endl;
|
||||
|
||||
if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
|
||||
vs << " vec2 texCoord = aTexCoord * uTextureRect.zw + uTextureRect.xy;" << endl;
|
||||
vs << " vec4 textureRect = uTextureRects[vertexID];" << endl;
|
||||
vs << " vec2 texCoord = aTexCoord.xy * textureRect.zw + textureRect.xy;" << endl;
|
||||
vs << " vTexCoord = (uTextureTransform * vec4(texCoord, 0.0, 1.0)).xy;" << endl;
|
||||
}
|
||||
|
||||
|
@ -52,10 +52,10 @@ public:
|
||||
|
||||
LayerTransform = 0,
|
||||
MaskTransform,
|
||||
LayerRect,
|
||||
LayerRects,
|
||||
MatrixProj,
|
||||
TextureTransform,
|
||||
TextureRect,
|
||||
TextureRects,
|
||||
RenderTargetOffset,
|
||||
LayerOpacity,
|
||||
Texture,
|
||||
@ -322,9 +322,12 @@ public:
|
||||
SetMatrixUniform(KnownUniform::MaskTransform, aMatrix);
|
||||
}
|
||||
|
||||
void SetLayerRect(const gfx::Rect& aRect) {
|
||||
float vals[4] = { float(aRect.x), float(aRect.y), float(aRect.width), float(aRect.height) };
|
||||
SetUniform(KnownUniform::LayerRect, 4, vals);
|
||||
void SetLayerRects(const gfx::Rect* aRects) {
|
||||
float vals[16] = { aRects[0].x, aRects[0].y, aRects[0].width, aRects[0].height,
|
||||
aRects[1].x, aRects[1].y, aRects[1].width, aRects[1].height,
|
||||
aRects[2].x, aRects[2].y, aRects[2].width, aRects[2].height,
|
||||
aRects[3].x, aRects[3].y, aRects[3].width, aRects[3].height };
|
||||
SetUniform(KnownUniform::LayerRects, 16, vals);
|
||||
}
|
||||
|
||||
void SetProjectionMatrix(const gfx::Matrix4x4& aMatrix) {
|
||||
@ -336,9 +339,12 @@ public:
|
||||
SetMatrixUniform(KnownUniform::TextureTransform, aMatrix);
|
||||
}
|
||||
|
||||
void SetTextureRect(const gfx::Rect& aRect) {
|
||||
float vals[4] = { float(aRect.x), float(aRect.y), float(aRect.width), float(aRect.height) };
|
||||
SetUniform(KnownUniform::TextureRect, 4, vals);
|
||||
void SetTextureRects(const gfx::Rect* aRects) {
|
||||
float vals[16] = { aRects[0].x, aRects[0].y, aRects[0].width, aRects[0].height,
|
||||
aRects[1].x, aRects[1].y, aRects[1].width, aRects[1].height,
|
||||
aRects[2].x, aRects[2].y, aRects[2].width, aRects[2].height,
|
||||
aRects[3].x, aRects[3].y, aRects[3].width, aRects[3].height };
|
||||
SetUniform(KnownUniform::TextureRects, 16, vals);
|
||||
}
|
||||
|
||||
void SetRenderOffset(const nsIntPoint& aOffset) {
|
||||
@ -471,6 +477,7 @@ protected:
|
||||
case 2: mGL->fUniform2fv(ku.mLocation, 1, ku.mValue.f16v); break;
|
||||
case 3: mGL->fUniform3fv(ku.mLocation, 1, ku.mValue.f16v); break;
|
||||
case 4: mGL->fUniform4fv(ku.mLocation, 1, ku.mValue.f16v); break;
|
||||
case 16: mGL->fUniform4fv(ku.mLocation, 4, ku.mValue.f16v); break;
|
||||
default:
|
||||
NS_NOTREACHED("Bogus aLength param");
|
||||
}
|
||||
|
@ -251,6 +251,15 @@ TextureImageTextureSourceOGL::Update(gfx::DataSourceSurface* aSurface,
|
||||
(mTexImage->GetSize() != size && !aSrcOffset) ||
|
||||
mTexImage->GetContentType() != gfx::ContentForFormat(aSurface->GetFormat())) {
|
||||
if (mFlags & TextureFlags::DISALLOW_BIGIMAGE) {
|
||||
GLint maxTextureSize;
|
||||
mGL->fGetIntegerv(LOCAL_GL_MAX_TEXTURE_SIZE, &maxTextureSize);
|
||||
if (size.width > maxTextureSize || size.height > maxTextureSize) {
|
||||
NS_WARNING("Texture exceeds maximum texture size, refusing upload");
|
||||
return false;
|
||||
}
|
||||
// Explicitly use CreateBasicTextureImage instead of CreateTextureImage,
|
||||
// because CreateTextureImage might still choose to create a tiled
|
||||
// texture image.
|
||||
mTexImage = CreateBasicTextureImage(mGL, size,
|
||||
gfx::ContentForFormat(aSurface->GetFormat()),
|
||||
WrapMode(mGL, mFlags),
|
||||
|
@ -123,6 +123,7 @@ gfxFontEntry::gfxFontEntry() :
|
||||
mHasSpaceFeaturesNonKerning(false),
|
||||
mSkipDefaultFeatureSpaceCheck(false),
|
||||
mCheckedForGraphiteTables(false),
|
||||
mCheckedForGraphiteSmallCaps(false),
|
||||
mHasCmapTable(false),
|
||||
mGrFaceInitialized(false),
|
||||
mCheckedForColorGlyph(false),
|
||||
@ -156,6 +157,7 @@ gfxFontEntry::gfxFontEntry(const nsAString& aName, bool aIsStandardFace) :
|
||||
mHasSpaceFeaturesNonKerning(false),
|
||||
mSkipDefaultFeatureSpaceCheck(false),
|
||||
mCheckedForGraphiteTables(false),
|
||||
mCheckedForGraphiteSmallCaps(false),
|
||||
mHasCmapTable(false),
|
||||
mGrFaceInitialized(false),
|
||||
mCheckedForColorGlyph(false),
|
||||
@ -879,6 +881,89 @@ gfxFontEntry::CheckForGraphiteTables()
|
||||
mHasGraphiteTables = HasFontTable(TRUETYPE_TAG('S','i','l','f'));
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFontEntry::SupportsOpenTypeSmallCaps(int32_t aScript)
|
||||
{
|
||||
if (!mSmallCapsSupport) {
|
||||
mSmallCapsSupport = new nsDataHashtable<nsUint32HashKey,bool>();
|
||||
}
|
||||
|
||||
bool result;
|
||||
if (mSmallCapsSupport->Get(uint32_t(aScript), &result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = false;
|
||||
|
||||
hb_face_t *face = GetHBFace();
|
||||
|
||||
if (hb_ot_layout_has_substitution(face)) {
|
||||
// Decide what harfbuzz script code will be used for shaping
|
||||
hb_script_t hbScript;
|
||||
if (aScript <= MOZ_SCRIPT_INHERITED) {
|
||||
// For unresolved "common" or "inherited" runs, default to Latin
|
||||
// for now. (Compare gfxHarfBuzzShaper.)
|
||||
hbScript = HB_SCRIPT_LATIN;
|
||||
} else {
|
||||
hbScript = hb_script_t(GetScriptTagForCode(aScript));
|
||||
}
|
||||
|
||||
// Get the OpenType tag(s) that match this script code
|
||||
hb_tag_t scriptTags[4] = {
|
||||
HB_TAG_NONE,
|
||||
HB_TAG_NONE,
|
||||
HB_TAG_NONE,
|
||||
HB_TAG_NONE
|
||||
};
|
||||
hb_ot_tags_from_script(hbScript, &scriptTags[0], &scriptTags[1]);
|
||||
|
||||
// Replace the first remaining NONE with DEFAULT
|
||||
hb_tag_t* scriptTag = &scriptTags[0];
|
||||
while (*scriptTag != HB_TAG_NONE) {
|
||||
++scriptTag;
|
||||
}
|
||||
*scriptTag = HB_OT_TAG_DEFAULT_SCRIPT;
|
||||
|
||||
// Now check for 'smcp' under the first of those scripts that is present
|
||||
const hb_tag_t kGSUB = HB_TAG('G','S','U','B');
|
||||
const hb_tag_t kSMCP = HB_TAG('s','m','c','p');
|
||||
scriptTag = &scriptTags[0];
|
||||
while (*scriptTag != HB_TAG_NONE) {
|
||||
unsigned int scriptIndex;
|
||||
if (hb_ot_layout_table_find_script(face, kGSUB, *scriptTag,
|
||||
&scriptIndex)) {
|
||||
if (hb_ot_layout_language_find_feature(face, kGSUB,
|
||||
scriptIndex,
|
||||
HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX,
|
||||
kSMCP, nullptr)) {
|
||||
result = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
++scriptTag;
|
||||
}
|
||||
}
|
||||
|
||||
hb_face_destroy(face);
|
||||
|
||||
mSmallCapsSupport->Put(uint32_t(aScript), result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFontEntry::SupportsGraphiteSmallCaps()
|
||||
{
|
||||
if (!mCheckedForGraphiteSmallCaps) {
|
||||
gr_face* face = GetGrFace();
|
||||
mHasGraphiteSmallCaps =
|
||||
gr_face_find_fref(face, TRUETYPE_TAG('s','m','c','p')) != nullptr;
|
||||
ReleaseGrFace(face);
|
||||
mCheckedForGraphiteSmallCaps = true;
|
||||
}
|
||||
return mHasGraphiteSmallCaps;
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFontEntry::GetColorLayersInfo(uint32_t aGlyphId,
|
||||
nsTArray<uint16_t>& aLayerGlyphs,
|
||||
@ -2011,6 +2096,7 @@ gfxFontShaper::MergeFontFeatures(
|
||||
if (styleRuleFeatures.IsEmpty() &&
|
||||
aFontFeatures.IsEmpty() &&
|
||||
!aDisableLigatures &&
|
||||
!aStyle->smallCaps &&
|
||||
numAlts == 0) {
|
||||
return false;
|
||||
}
|
||||
@ -2022,6 +2108,10 @@ gfxFontShaper::MergeFontFeatures(
|
||||
aMergedFeatures.Put(HB_TAG('c','l','i','g'), 0);
|
||||
}
|
||||
|
||||
if (aStyle->smallCaps) {
|
||||
aMergedFeatures.Put(HB_TAG('s','m','c','p'), 1);
|
||||
}
|
||||
|
||||
// add feature values from font
|
||||
uint32_t i, count;
|
||||
|
||||
@ -2608,6 +2698,16 @@ gfxFont::SpaceMayParticipateInShaping(int32_t aRunScript)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFont::SupportsSmallCaps(int32_t aScript)
|
||||
{
|
||||
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
|
||||
return GetFontEntry()->SupportsGraphiteSmallCaps();
|
||||
}
|
||||
|
||||
return GetFontEntry()->SupportsOpenTypeSmallCaps(aScript);
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFont::HasFeatureSet(uint32_t aFeature, bool& aFeatureOn)
|
||||
{
|
||||
@ -5444,13 +5544,14 @@ gfxFontGroup::InitScriptRun(gfxContext *aContext,
|
||||
|
||||
// create the glyph run for this range
|
||||
if (matchedFont) {
|
||||
if (mStyle.smallCaps) {
|
||||
if (!matchedFont->InitSmallCapsRun(aContext, aTextRun,
|
||||
aString + runStart,
|
||||
aOffset + runStart,
|
||||
matchedLength,
|
||||
range.matchType,
|
||||
aRunScript)) {
|
||||
if (mStyle.smallCaps &&
|
||||
!matchedFont->SupportsSmallCaps(aRunScript)) {
|
||||
if (!matchedFont->InitFakeSmallCapsRun(aContext, aTextRun,
|
||||
aString + runStart,
|
||||
aOffset + runStart,
|
||||
matchedLength,
|
||||
range.matchType,
|
||||
aRunScript)) {
|
||||
matchedFont = nullptr;
|
||||
}
|
||||
} else {
|
||||
@ -5549,28 +5650,28 @@ gfxFontGroup::InitScriptRun(gfxContext *aContext,
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFont::InitSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const uint8_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript)
|
||||
gfxFont::InitFakeSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const uint8_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript)
|
||||
{
|
||||
NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aText),
|
||||
aLength);
|
||||
return InitSmallCapsRun(aContext, aTextRun, unicodeString.get(),
|
||||
aOffset, aLength, aMatchType, aScript);
|
||||
return InitFakeSmallCapsRun(aContext, aTextRun, unicodeString.get(),
|
||||
aOffset, aLength, aMatchType, aScript);
|
||||
}
|
||||
|
||||
bool
|
||||
gfxFont::InitSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const char16_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript)
|
||||
gfxFont::InitFakeSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const char16_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript)
|
||||
{
|
||||
bool ok = true;
|
||||
|
||||
|
@ -273,6 +273,9 @@ public:
|
||||
bool IgnoreGDEF() const { return mIgnoreGDEF; }
|
||||
bool IgnoreGSUB() const { return mIgnoreGSUB; }
|
||||
|
||||
bool SupportsOpenTypeSmallCaps(int32_t aScript);
|
||||
bool SupportsGraphiteSmallCaps();
|
||||
|
||||
virtual bool IsSymbolFont();
|
||||
|
||||
virtual bool HasFontTable(uint32_t aTableTag);
|
||||
@ -541,6 +544,8 @@ public:
|
||||
bool mSkipDefaultFeatureSpaceCheck : 1;
|
||||
bool mHasGraphiteTables : 1;
|
||||
bool mCheckedForGraphiteTables : 1;
|
||||
bool mHasGraphiteSmallCaps : 1;
|
||||
bool mCheckedForGraphiteSmallCaps : 1;
|
||||
bool mHasCmapTable : 1;
|
||||
bool mGrFaceInitialized : 1;
|
||||
bool mCheckedForColorGlyph : 1;
|
||||
@ -562,6 +567,7 @@ public:
|
||||
nsTArray<gfxFont*> mFontsUsingSVGGlyphs;
|
||||
nsAutoPtr<gfxMathTable> mMathTable;
|
||||
nsTArray<gfxFontFeature> mFeatureSettings;
|
||||
nsAutoPtr<nsDataHashtable<nsUint32HashKey,bool>> mSmallCapsSupport;
|
||||
uint32_t mLanguageOverride;
|
||||
|
||||
// Color Layer font support
|
||||
@ -1608,6 +1614,9 @@ public:
|
||||
return mFontEntry->HasGraphiteTables();
|
||||
}
|
||||
|
||||
// whether the font supports "real" small caps or should fake them
|
||||
bool SupportsSmallCaps(int32_t aScript);
|
||||
|
||||
// Subclasses may choose to look up glyph ids for characters.
|
||||
// If they do not override this, gfxHarfBuzzShaper will fetch the cmap
|
||||
// table and use that.
|
||||
@ -1808,21 +1817,21 @@ public:
|
||||
return mFontEntry->GetUVSGlyph(aCh, aVS);
|
||||
}
|
||||
|
||||
bool InitSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const uint8_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript);
|
||||
bool InitFakeSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const uint8_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript);
|
||||
|
||||
bool InitSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const char16_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript);
|
||||
bool InitFakeSmallCapsRun(gfxContext *aContext,
|
||||
gfxTextRun *aTextRun,
|
||||
const char16_t *aText,
|
||||
uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
uint8_t aMatchType,
|
||||
int32_t aScript);
|
||||
|
||||
// call the (virtual) InitTextRun method to do glyph generation/shaping,
|
||||
// limiting the length of text passed by processing the run in multiple
|
||||
|
@ -412,7 +412,7 @@ VectorImage::HeapSizeOfVectorImageDocument(nsACString* aDocURL) const
|
||||
}
|
||||
|
||||
nsWindowSizes windowSizes(WindowsMallocSizeOf);
|
||||
doc->DocAddSizeOfExcludingThis(&windowSizes);
|
||||
doc->DocAddSizeOfIncludingThis(&windowSizes);
|
||||
return windowSizes.getTotalSize();
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,35 @@ function radd_object(i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
var uceFault_sub_number = eval(uneval(uceFault).replace('uceFault', 'uceFault_sub_number'));
|
||||
function rsub_number(i) {
|
||||
var x = 1 - i;
|
||||
if (uceFault_sub_number(i) || uceFault_sub_number(i))
|
||||
assertEq(x, -98 /* = 1 - 99 */);
|
||||
return i;
|
||||
}
|
||||
|
||||
var uceFault_sub_float = eval(uneval(uceFault).replace('uceFault', 'uceFault_sub_float'));
|
||||
function rsub_float(i) {
|
||||
var t = Math.fround(1/3);
|
||||
var fi = Math.fround(i);
|
||||
var x = Math.fround(Math.fround(Math.fround(Math.fround(t - fi) - t) - fi) - t);
|
||||
if (uceFault_sub_float(i) || uceFault_sub_float(i))
|
||||
assertEq(x, -198.3333282470703); /* != -198.33333334326744 (when computed with double subtractions) */
|
||||
return i;
|
||||
}
|
||||
|
||||
var uceFault_sub_object = eval(uneval(uceFault).replace('uceFault', 'uceFault_sub_object'));
|
||||
function rsub_object(i) {
|
||||
var t = i;
|
||||
var o = { valueOf: function () { return t; } };
|
||||
var x = o - i; /* computed with t == i, not 1000 */
|
||||
t = 1000;
|
||||
if (uceFault_sub_object(i) || uceFault_sub_object(i))
|
||||
assertEq(x, 0);
|
||||
return i;
|
||||
}
|
||||
|
||||
for (i = 0; i < 100; i++) {
|
||||
rbitnot_number(i);
|
||||
rbitnot_object(i);
|
||||
@ -180,6 +209,9 @@ for (i = 0; i < 100; i++) {
|
||||
radd_float(i);
|
||||
radd_string(i);
|
||||
radd_object(i);
|
||||
rsub_number(i);
|
||||
rsub_float(i);
|
||||
rsub_object(i);
|
||||
}
|
||||
|
||||
// Test that we can refer multiple time to the same recover instruction, as well
|
||||
|
@ -4300,6 +4300,11 @@ class MSub : public MBinaryArithInstruction
|
||||
void computeRange(TempAllocator &alloc);
|
||||
bool truncate(TruncateKind kind);
|
||||
TruncateKind operandTruncateKind(size_t index) const;
|
||||
|
||||
bool writeRecoverData(CompactBufferWriter &writer) const;
|
||||
bool canRecoverOnBailout() const {
|
||||
return specialization_ != MIRType_None;
|
||||
}
|
||||
};
|
||||
|
||||
class MMul : public MBinaryArithInstruction
|
||||
|
@ -330,6 +330,40 @@ RAdd::recover(JSContext *cx, SnapshotIterator &iter) const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MSub::writeRecoverData(CompactBufferWriter &writer) const
|
||||
{
|
||||
MOZ_ASSERT(canRecoverOnBailout());
|
||||
writer.writeUnsigned(uint32_t(RInstruction::Recover_Sub));
|
||||
writer.writeByte(specialization_ == MIRType_Float32);
|
||||
return true;
|
||||
}
|
||||
|
||||
RSub::RSub(CompactBufferReader &reader)
|
||||
{
|
||||
isFloatOperation_ = reader.readByte();
|
||||
}
|
||||
|
||||
bool
|
||||
RSub::recover(JSContext *cx, SnapshotIterator &iter) const
|
||||
{
|
||||
RootedValue lhs(cx, iter.read());
|
||||
RootedValue rhs(cx, iter.read());
|
||||
RootedValue result(cx);
|
||||
|
||||
MOZ_ASSERT(!lhs.isObject() && !rhs.isObject());
|
||||
if (!js::SubValues(cx, &lhs, &rhs, &result))
|
||||
return false;
|
||||
|
||||
// MIRType_Float32 is a specialization embedding the fact that the result is
|
||||
// rounded to a Float32.
|
||||
if (isFloatOperation_ && !RoundFloat32(cx, result, &result))
|
||||
return false;
|
||||
|
||||
iter.storeInstructionResult(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MNewObject::writeRecoverData(CompactBufferWriter &writer) const
|
||||
{
|
||||
|
@ -25,6 +25,7 @@ namespace jit {
|
||||
_(Rsh) \
|
||||
_(Ursh) \
|
||||
_(Add) \
|
||||
_(Sub) \
|
||||
_(NewObject) \
|
||||
_(NewDerivedTypedObject)
|
||||
|
||||
@ -181,6 +182,21 @@ class RAdd MOZ_FINAL : public RInstruction
|
||||
bool recover(JSContext *cx, SnapshotIterator &iter) const;
|
||||
};
|
||||
|
||||
class RSub MOZ_FINAL : public RInstruction
|
||||
{
|
||||
private:
|
||||
bool isFloatOperation_;
|
||||
|
||||
public:
|
||||
RINSTRUCTION_HEADER_(Sub)
|
||||
|
||||
virtual uint32_t numOperands() const {
|
||||
return 2;
|
||||
}
|
||||
|
||||
bool recover(JSContext *cx, SnapshotIterator &iter) const;
|
||||
};
|
||||
|
||||
class RNewObject MOZ_FINAL : public RInstruction
|
||||
{
|
||||
private:
|
||||
|
@ -64,7 +64,7 @@ function DocumentWrite(s)
|
||||
}
|
||||
|
||||
function print() {
|
||||
var s = '';
|
||||
var s = 'TEST-INFO | ';
|
||||
var a;
|
||||
for (var i = 0; i < arguments.length; i++)
|
||||
{
|
||||
|
@ -1840,6 +1840,9 @@ AddTransformedBoundsToRegion(const nsIntRegion& aRegion,
|
||||
nsIntRegion* aDest)
|
||||
{
|
||||
nsIntRect bounds = aRegion.GetBounds();
|
||||
if (bounds.IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
gfxRect transformed =
|
||||
aTransform.TransformBounds(gfxRect(bounds.x, bounds.y, bounds.width, bounds.height));
|
||||
transformed.RoundOut();
|
||||
|
@ -2156,6 +2156,68 @@ FindTileStart(nscoord aDirtyCoord, nscoord aTilePos, nscoord aTileDim)
|
||||
return NSToCoordRound(multiples*aTileDim + aTilePos);
|
||||
}
|
||||
|
||||
static gfxFloat
|
||||
LinearGradientStopPositionForPoint(const gfxPoint& aGradientStart,
|
||||
const gfxPoint& aGradientEnd,
|
||||
const gfxPoint& aPoint)
|
||||
{
|
||||
gfxPoint d = aGradientEnd - aGradientStart;
|
||||
gfxPoint p = aPoint - aGradientStart;
|
||||
/**
|
||||
* Compute a parameter t such that a line perpendicular to the
|
||||
* d vector, passing through aGradientStart + d*t, also
|
||||
* passes through aPoint.
|
||||
*
|
||||
* t is given by
|
||||
* (p.x - d.x*t)*d.x + (p.y - d.y*t)*d.y = 0
|
||||
*
|
||||
* Solving for t we get
|
||||
* numerator = d.x*p.x + d.y*p.y
|
||||
* denominator = d.x^2 + d.y^2
|
||||
* t = numerator/denominator
|
||||
*
|
||||
* In nsCSSRendering::PaintGradient we know the length of d
|
||||
* is not zero.
|
||||
*/
|
||||
double numerator = d.x * p.x + d.y * p.y;
|
||||
double denominator = d.x * d.x + d.y * d.y;
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
static bool
|
||||
RectIsBeyondLinearGradientEdge(const gfxRect& aRect,
|
||||
const gfxMatrix& aPatternMatrix,
|
||||
const nsTArray<ColorStop>& aStops,
|
||||
const gfxPoint& aGradientStart,
|
||||
const gfxPoint& aGradientEnd,
|
||||
gfxRGBA* aOutEdgeColor)
|
||||
{
|
||||
gfxFloat topLeft = LinearGradientStopPositionForPoint(
|
||||
aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopLeft()));
|
||||
gfxFloat topRight = LinearGradientStopPositionForPoint(
|
||||
aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopRight()));
|
||||
gfxFloat bottomLeft = LinearGradientStopPositionForPoint(
|
||||
aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.BottomLeft()));
|
||||
gfxFloat bottomRight = LinearGradientStopPositionForPoint(
|
||||
aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.BottomRight()));
|
||||
|
||||
const ColorStop& firstStop = aStops[0];
|
||||
if (topLeft < firstStop.mPosition && topRight < firstStop.mPosition &&
|
||||
bottomLeft < firstStop.mPosition && bottomRight < firstStop.mPosition) {
|
||||
*aOutEdgeColor = firstStop.mColor;
|
||||
return true;
|
||||
}
|
||||
|
||||
const ColorStop& lastStop = aStops.LastElement();
|
||||
if (topLeft >= lastStop.mPosition && topRight >= lastStop.mPosition &&
|
||||
bottomLeft >= lastStop.mPosition && bottomRight >= lastStop.mPosition) {
|
||||
*aOutEdgeColor = lastStop.mColor;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
nsCSSRendering::PaintGradient(nsPresContext* aPresContext,
|
||||
nsRenderingContext& aRenderingContext,
|
||||
@ -2362,11 +2424,13 @@ nsCSSRendering::PaintGradient(nsPresContext* aPresContext,
|
||||
nsRefPtr<gfxPattern> gradientPattern;
|
||||
bool forceRepeatToCoverTiles = false;
|
||||
gfxMatrix matrix;
|
||||
gfxPoint gradientStart;
|
||||
gfxPoint gradientEnd;
|
||||
if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) {
|
||||
// Compute the actual gradient line ends we need to pass to cairo after
|
||||
// stops have been normalized.
|
||||
gfxPoint gradientStart = lineStart + (lineEnd - lineStart)*stopOrigin;
|
||||
gfxPoint gradientEnd = lineStart + (lineEnd - lineStart)*stopEnd;
|
||||
gradientStart = lineStart + (lineEnd - lineStart)*stopOrigin;
|
||||
gradientEnd = lineStart + (lineEnd - lineStart)*stopEnd;
|
||||
gfxPoint gradientStopStart = lineStart + (lineEnd - lineStart)*firstStop;
|
||||
gfxPoint gradientStopEnd = lineStart + (lineEnd - lineStart)*lastStop;
|
||||
|
||||
@ -2488,6 +2552,9 @@ nsCSSRendering::PaintGradient(nsPresContext* aPresContext,
|
||||
|
||||
gfxRect areaToFill =
|
||||
nsLayoutUtils::RectToGfxRect(aFillArea, appUnitsPerDevPixel);
|
||||
gfxRect dirtyAreaToFill = nsLayoutUtils::RectToGfxRect(dirty, appUnitsPerDevPixel);
|
||||
dirtyAreaToFill.RoundOut();
|
||||
|
||||
gfxMatrix ctm = ctx->CurrentMatrix();
|
||||
bool isCTMPreservingAxisAlignedRectangles = ctm.PreservesAxisAlignedRectangles();
|
||||
|
||||
@ -2535,8 +2602,18 @@ nsCSSRendering::PaintGradient(nsPresContext* aPresContext,
|
||||
}
|
||||
ctx->NewPath();
|
||||
ctx->Rectangle(fillRect);
|
||||
ctx->Translate(tileRect.TopLeft());
|
||||
ctx->SetPattern(gradientPattern);
|
||||
|
||||
gfxRect dirtyFillRect = fillRect.Intersect(dirtyAreaToFill);
|
||||
gfxRect fillRectRelativeToTile = dirtyFillRect - tileRect.TopLeft();
|
||||
gfxRGBA edgeColor;
|
||||
if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && !isRepeat &&
|
||||
RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, stops,
|
||||
gradientStart, gradientEnd, &edgeColor)) {
|
||||
ctx->SetColor(edgeColor);
|
||||
} else {
|
||||
ctx->Translate(tileRect.TopLeft());
|
||||
ctx->SetPattern(gradientPattern);
|
||||
}
|
||||
ctx->Fill();
|
||||
ctx->SetMatrix(ctm);
|
||||
}
|
||||
|
@ -1623,12 +1623,10 @@ GetAnimatedGeometryRootForFrame(nsIFrame* aFrame,
|
||||
if (!parent)
|
||||
break;
|
||||
nsIAtom* parentType = parent->GetType();
|
||||
#ifdef ANDROID
|
||||
// Treat the slider thumb as being as an active scrolled root
|
||||
// on mobile so that it can move without repainting.
|
||||
// so that it can move without repainting.
|
||||
if (parentType == nsGkAtoms::sliderFrame)
|
||||
break;
|
||||
#endif
|
||||
// Sticky frames are active if their nearest scrollable frame
|
||||
// is also active, just keep a record of sticky frames that we
|
||||
// encounter for now.
|
||||
|
@ -2183,7 +2183,7 @@ void
|
||||
ScrollFrameHelper::AppendScrollPartsTo(nsDisplayListBuilder* aBuilder,
|
||||
const nsRect& aDirtyRect,
|
||||
const nsDisplayListSet& aLists,
|
||||
bool& aCreateLayer,
|
||||
bool aCreateLayer,
|
||||
bool aPositioned)
|
||||
{
|
||||
nsITheme* theme = mOuter->PresContext()->GetTheme();
|
||||
@ -2438,9 +2438,9 @@ ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
// We put scrollbars in their own layers when this is the root scroll
|
||||
// frame and we are a toplevel content document. In this situation, the
|
||||
// scrollbar(s) would normally be assigned their own layer anyway, since
|
||||
// We put non-overlay scrollbars in their own layers when this is the root
|
||||
// scroll frame and we are a toplevel content document. In this situation,
|
||||
// the scrollbar(s) would normally be assigned their own layer anyway, since
|
||||
// they're not scrolled with the rest of the document. But when both
|
||||
// scrollbars are visible, the layer's visible rectangle would be the size
|
||||
// of the viewport, so most layer implementations would create a layer buffer
|
||||
@ -2475,16 +2475,9 @@ ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
||||
mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame,
|
||||
aDirtyRect, aLists);
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
// TODO: only layerize the overlay scrollbars if this scrollframe can be
|
||||
// panned asynchronously. For now just always layerize on B2G because.
|
||||
// that's where we want the layerized scrollbars
|
||||
createLayersForScrollbars = true;
|
||||
#endif
|
||||
if (addScrollBars) {
|
||||
// Add overlay scrollbars.
|
||||
AppendScrollPartsTo(aBuilder, aDirtyRect, aLists, createLayersForScrollbars,
|
||||
true);
|
||||
AppendScrollPartsTo(aBuilder, aDirtyRect, aLists, true, true);
|
||||
}
|
||||
|
||||
return;
|
||||
@ -2685,14 +2678,9 @@ ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
||||
scrolledContent.BorderBackground()->AppendNewToBottom(layerItem);
|
||||
}
|
||||
// Now display overlay scrollbars and the resizer, if we have one.
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
// TODO: only layerize the overlay scrollbars if this scrollframe can be
|
||||
// panned asynchronously. For now just always layerize on B2G because.
|
||||
// that's where we want the layerized scrollbars
|
||||
createLayersForScrollbars = true;
|
||||
#endif
|
||||
AppendScrollPartsTo(aBuilder, aDirtyRect, scrolledContent,
|
||||
createLayersForScrollbars, true);
|
||||
// Always create layers for these, so that we don't create a giant layer
|
||||
// covering the whole scrollport if both scrollbars are visible.
|
||||
AppendScrollPartsTo(aBuilder, aDirtyRect, scrolledContent, true, true);
|
||||
scrolledContent.MoveTo(aLists);
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ public:
|
||||
void AppendScrollPartsTo(nsDisplayListBuilder* aBuilder,
|
||||
const nsRect& aDirtyRect,
|
||||
const nsDisplayListSet& aLists,
|
||||
bool& aCreateLayer,
|
||||
bool aCreateLayer,
|
||||
bool aPositioned);
|
||||
|
||||
bool GetBorderRadii(const nsSize& aFrameSize, const nsSize& aBorderArea,
|
||||
|
@ -592,12 +592,14 @@ hb_ot_layout_collect_lookups
|
||||
hb_ot_layout_feature_get_lookups
|
||||
hb_ot_layout_has_positioning
|
||||
hb_ot_layout_has_substitution
|
||||
hb_ot_layout_language_find_feature
|
||||
hb_ot_layout_language_get_feature_indexes
|
||||
hb_ot_layout_language_get_feature_tags
|
||||
hb_ot_layout_language_get_required_feature_index
|
||||
hb_ot_layout_lookup_collect_glyphs
|
||||
hb_ot_layout_script_get_language_tags
|
||||
hb_ot_layout_table_choose_script
|
||||
hb_ot_layout_table_find_script
|
||||
hb_ot_layout_table_get_script_tags
|
||||
hb_ot_tag_to_language
|
||||
hb_ot_tag_to_script
|
||||
|
@ -845,7 +845,7 @@ skip-if(B2G&&browserIsRemote) == 401946-1.xul about:blank # bug 974780
|
||||
== 402567-1.html 402567-1-ref.html
|
||||
== 402567-2.html 402567-2-ref.html
|
||||
== 402567-3.html 402567-3-ref.html
|
||||
skip-if(B2G) == 402567-4.html 402567-4-ref.html
|
||||
skip-if(B2G) fuzzy-if(gtk2Widget,2,40) == 402567-4.html 402567-4-ref.html
|
||||
random-if(B2G&&browserIsRemote) == 402629-1.html 402629-1-ref.html
|
||||
random-if(B2G&&browserIsRemote) == 402629-2.html 402629-2-ref.html
|
||||
random-if(B2G&&browserIsRemote) == 402629-3.html 402629-3-ref.html
|
||||
|
@ -9,11 +9,11 @@
|
||||
<canvas id="canvas1" width="400" height="400"></canvas>
|
||||
<script type="text/javascript">
|
||||
var ctx = document.getElementById('canvas1').getContext('2d');
|
||||
ctx.font = "40px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "40px monospace";
|
||||
ctx.fillText("lowercase", 50, 50);
|
||||
ctx.font = "32px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "32px monospace";
|
||||
ctx.fillText("SMALLCAPS", 50, 100);
|
||||
ctx.font = "40px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "40px monospace";
|
||||
ctx.fillText("CAPITALS", 50, 150);
|
||||
</script>
|
||||
</body>
|
||||
|
@ -9,11 +9,11 @@
|
||||
<canvas id="canvas1" width="400" height="400"></canvas>
|
||||
<script type="text/javascript">
|
||||
var ctx = document.getElementById('canvas1').getContext('2d');
|
||||
ctx.font = "40px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "40px monospace";
|
||||
ctx.fillText("lowercase", 50, 50);
|
||||
ctx.font = "small-caps 40px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "small-caps 40px monospace";
|
||||
ctx.fillText("smallcaps", 50, 100);
|
||||
ctx.font = "small-caps 40px arial, helvetica, roboto, droid sans, sans-serif";
|
||||
ctx.font = "small-caps 40px monospace";
|
||||
ctx.fillText("CAPITALS", 50, 150);
|
||||
</script>
|
||||
</body>
|
||||
|
94
layout/reftests/fonts/sil/Charis-license.txt
Executable file
94
layout/reftests/fonts/sil/Charis-license.txt
Executable file
@ -0,0 +1,94 @@
|
||||
This Font Software is Copyright (c) 1997-2013, SIL International (http://scripts.sil.org/)
|
||||
with Reserved Font Names "Charis" and "SIL".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
layout/reftests/fonts/sil/CharisSIL-R.ttf
Executable file
BIN
layout/reftests/fonts/sil/CharisSIL-R.ttf
Executable file
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
skip-if(B2G) fails-if(Android) == resize.html resize-ref.html
|
||||
# an offset seems to apply to the native resizer on windows so skip this test for now
|
||||
skip-if(B2G) fails-if(Android) skip-if(winWidget) == resize-background.html resize-background-ref.html
|
||||
skip-if(B2G) fails-if(Android) skip-if(winWidget) fuzzy-if(cocoaWidget,1,33) == resize-background.html resize-background-ref.html
|
||||
skip-if(B2G) fails-if(Android) != ltr.html rtl.html
|
||||
skip-if(B2G) fails-if(Android) != ltr-scrollbar.html rtl-scrollbar.html
|
||||
skip-if(B2G) fails-if(Android) != in-ltr-doc-scrollbar.html in-rtl-doc-scrollbar.html
|
||||
|
@ -17,7 +17,7 @@ random-if(d2d) == element-paint-transform-02.html element-paint-transform-02-ref
|
||||
== element-paint-background-size-02.html element-paint-background-size-02-ref.html
|
||||
== element-paint-transform-repeated.html element-paint-transform-repeated-ref.html
|
||||
fuzzy-if(d2d,255,24) == element-paint-transform-03.html element-paint-transform-03-ref.html
|
||||
== element-paint-native-widget.html element-paint-native-widget-ref.html
|
||||
fuzzy-if(gtk2Widget,1,32) fuzzy-if(cocoaWidget,1,106) == element-paint-native-widget.html element-paint-native-widget-ref.html
|
||||
== element-paint-subimage-sampling-restriction.html about:blank
|
||||
== element-paint-clippath.html element-paint-clippath-ref.html
|
||||
== element-paint-sharpness-01a.html element-paint-sharpness-01b.html
|
||||
|
@ -3,8 +3,8 @@ skip-if(B2G) == ellipsis-font-fallback.html ellipsis-font-fallback-ref.html
|
||||
skip-if(B2G) HTTP(..) == marker-basic.html marker-basic-ref.html
|
||||
skip-if(B2G) HTTP(..) == marker-string.html marker-string-ref.html
|
||||
skip-if(Android||B2G) HTTP(..) == bidi-simple.html bidi-simple-ref.html # Fails on Android due to anti-aliasing
|
||||
skip-if(!gtk2Widget) HTTP(..) == bidi-simple-scrolled.html bidi-simple-scrolled-ref.html # Fails on Windows and OSX due to anti-aliasing
|
||||
skip-if(B2G) fuzzy-if(Android&&AndroidVersion<15,9,2545) fuzzy-if(Android&&AndroidVersion>=15,24,4000) HTTP(..) == scroll-rounding.html scroll-rounding-ref.html # bug 760264
|
||||
skip-if(!gtk2Widget) fuzzy-if(gtk2Widget,1,104) HTTP(..) == bidi-simple-scrolled.html bidi-simple-scrolled-ref.html # Fails on Windows and OSX due to anti-aliasing
|
||||
skip-if(B2G) fuzzy-if(Android&&AndroidVersion<15,9,2545) fuzzy-if(Android&&AndroidVersion>=15,24,4000) fuzzy-if(cocoaWidget,1,40) HTTP(..) == scroll-rounding.html scroll-rounding-ref.html # bug 760264
|
||||
fuzzy-if(OSX==10.8,1,1) HTTP(..) == anonymous-block.html anonymous-block-ref.html
|
||||
skip-if(B2G) HTTP(..) == false-marker-overlap.html false-marker-overlap-ref.html
|
||||
HTTP(..) == visibility-hidden.html visibility-hidden-ref.html
|
||||
|
22
layout/reftests/text-transform/fake-small-caps-1-ref.html
Normal file
22
layout/reftests/text-transform/fake-small-caps-1-ref.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that does NOT support the 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/dejavu-sans/DejaVuSans.ttf);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
span {
|
||||
font-size: 80px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>S<span>MALL</span> C<span>APS</span>
|
||||
</body>
|
||||
</html>
|
19
layout/reftests/text-transform/fake-small-caps-1.html
Normal file
19
layout/reftests/text-transform/fake-small-caps-1.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that does NOT support the 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/dejavu-sans/DejaVuSans.ttf);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="font-variant:small-caps">Small Caps
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the Graphite 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/sil/CharisSIL-R.ttf);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
span {
|
||||
font-size: 80px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>S<span>MALL</span> C<span>APS</span>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the Graphite 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/sil/CharisSIL-R.ttf);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
div {
|
||||
-moz-font-feature-settings: 'smcp' on;
|
||||
font-feature-settings: 'smcp' on;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>Small Caps
|
||||
</body>
|
||||
</html>
|
19
layout/reftests/text-transform/graphite-small-caps-1.html
Normal file
19
layout/reftests/text-transform/graphite-small-caps-1.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the Graphite 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/sil/CharisSIL-R.ttf);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="font-variant:small-caps">Small Caps
|
||||
</body>
|
||||
</html>
|
@ -4,8 +4,13 @@
|
||||
<title>Test for Greek small-caps</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<style type="text/css">
|
||||
/* use a known font, to avoid dependency on platform font behavior */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/dejavu-sans/DejaVuSans.ttf);
|
||||
}
|
||||
div {
|
||||
font: 150% serif;
|
||||
font: 150% test;
|
||||
margin: 1em;
|
||||
}
|
||||
span {
|
||||
|
@ -4,8 +4,13 @@
|
||||
<title>Test for Greek small-caps</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<style type="text/css">
|
||||
/* use a known font, to avoid dependency on platform font behavior */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/dejavu-sans/DejaVuSans.ttf);
|
||||
}
|
||||
div {
|
||||
font: 150% serif;
|
||||
font: 150% test;
|
||||
font-variant: small-caps;
|
||||
margin: 1em;
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the OpenType 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/LinLibertine_Re-4.7.5.woff);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
span {
|
||||
font-size: 80px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>S<span>MALL</span> C<span>APS</span>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the OpenType 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/LinLibertine_Re-4.7.5.woff);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
div {
|
||||
-moz-font-feature-settings: 'smcp' on;
|
||||
font-feature-settings: 'smcp' on;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>Small Caps
|
||||
</body>
|
||||
</html>
|
19
layout/reftests/text-transform/opentype-small-caps-1.html
Normal file
19
layout/reftests/text-transform/opentype-small-caps-1.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* load a font that supports the OpenType 'smcp' feature */
|
||||
@font-face {
|
||||
font-family: test;
|
||||
src: url(../fonts/LinLibertine_Re-4.7.5.woff);
|
||||
}
|
||||
body {
|
||||
font: 100px test;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="font-variant:small-caps">Small Caps
|
||||
</body>
|
||||
</html>
|
@ -8,7 +8,12 @@
|
||||
== capitalize-7a.html capitalize-7-ref.html
|
||||
== lowercase-1.html lowercase-ref.html
|
||||
== lowercase-sigma-1.html lowercase-sigma-1-ref.html
|
||||
== small-caps-1.html small-caps-1-ref.html
|
||||
fails-if(B2G) random-if(winWidget) == small-caps-1.html small-caps-1-ref.html # fails if default font supports 'smcp'
|
||||
HTTP(..) == fake-small-caps-1.html fake-small-caps-1-ref.html
|
||||
HTTP(..) == opentype-small-caps-1.html opentype-small-caps-1-ref.html
|
||||
HTTP(..) != opentype-small-caps-1.html opentype-small-caps-1-notref.html
|
||||
HTTP(..) == graphite-small-caps-1.html graphite-small-caps-1-ref.html
|
||||
HTTP(..) != graphite-small-caps-1.html graphite-small-caps-1-notref.html
|
||||
== uppercase-1.html uppercase-ref.html
|
||||
== uppercase-szlig-1.html uppercase-szlig-ref.html
|
||||
# these use DejaVu Sans via @font-face for consistency of results
|
||||
@ -22,7 +27,7 @@ skip-if(B2G) HTTP(..) == all-title.html all-title-ref.html # bug 773482
|
||||
HTTP(..) != small-caps-turkish-1.html small-caps-turkish-1-notref.html
|
||||
== greek-uppercase-1.html greek-uppercase-1-ref.html
|
||||
== greek-uppercase-2.html greek-uppercase-2-ref.html
|
||||
== greek-small-caps-1.html greek-small-caps-1-ref.html
|
||||
HTTP(..) == greek-small-caps-1.html greek-small-caps-1-ref.html
|
||||
== fullwidth-1.html fullwidth-1-ref.html
|
||||
== fullwidth-2.html fullwidth-2-ref.html
|
||||
== fullwidth-all.html fullwidth-all-ref.html
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" align="left">
|
||||
<hbox style="font: 40px arial, helvetica, roboto, droid sans, sans-serif;" align="baseline">
|
||||
<hbox style="font: 40px monospace;" align="baseline">
|
||||
<label value="lowercase"/>
|
||||
<label value="SMALLCAPS" style="font-size: 32px;"/>
|
||||
<label value="CAPITALS"/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" align="left">
|
||||
<hbox style="font: 40px arial, helvetica, roboto, droid sans, sans-serif;">
|
||||
<hbox style="font: 40px monospace;">
|
||||
<label value="lowercase" style="font-variant: normal;"/>
|
||||
<label value="smallcaps" style="font-variant: small-caps;"/>
|
||||
<label value="CAPITALS" style="font-variant: small-caps;"/>
|
||||
|
@ -664,11 +664,10 @@ nsSliderFrame::CurrentPositionChanged()
|
||||
else
|
||||
newThumbRect.y = clientRect.y + NSToCoordRound(pos * mRatio);
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
// avoid putting the scroll thumb at subpixel positions which cause needless invalidations
|
||||
nscoord appUnitsPerPixel = PresContext()->AppUnitsPerDevPixel();
|
||||
newThumbRect = newThumbRect.ToNearestPixels(appUnitsPerPixel).ToAppUnits(appUnitsPerPixel);
|
||||
#endif
|
||||
|
||||
// set the rect
|
||||
thumbFrame->SetRect(newThumbRect);
|
||||
|
||||
|
@ -1054,7 +1054,7 @@ CacheFile::GetChunkLocked(uint32_t aIndex, ECallerType aCaller,
|
||||
mChunks.Put(aIndex, chunk);
|
||||
mCachedChunks.Remove(aIndex);
|
||||
chunk->mFile = this;
|
||||
chunk->mRemovingChunk = false;
|
||||
chunk->mActiveChunk = true;
|
||||
|
||||
MOZ_ASSERT(chunk->IsReady());
|
||||
|
||||
@ -1084,6 +1084,7 @@ CacheFile::GetChunkLocked(uint32_t aIndex, ECallerType aCaller,
|
||||
|
||||
chunk = new CacheFileChunk(this, aIndex);
|
||||
mChunks.Put(aIndex, chunk);
|
||||
chunk->mActiveChunk = true;
|
||||
|
||||
LOG(("CacheFile::GetChunkLocked() - Reading newly created chunk %p from "
|
||||
"the disk [this=%p]", chunk.get(), this));
|
||||
@ -1114,6 +1115,7 @@ CacheFile::GetChunkLocked(uint32_t aIndex, ECallerType aCaller,
|
||||
// this listener is going to write to the chunk
|
||||
chunk = new CacheFileChunk(this, aIndex);
|
||||
mChunks.Put(aIndex, chunk);
|
||||
chunk->mActiveChunk = true;
|
||||
|
||||
LOG(("CacheFile::GetChunkLocked() - Created new empty chunk %p [this=%p]",
|
||||
chunk.get(), this));
|
||||
@ -1298,7 +1300,7 @@ CacheFile::MustKeepCachedChunk(uint32_t aIndex)
|
||||
}
|
||||
|
||||
nsresult
|
||||
CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
CacheFile::DeactivateChunk(CacheFileChunk *aChunk)
|
||||
{
|
||||
nsresult rv;
|
||||
|
||||
@ -1308,7 +1310,7 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
{
|
||||
CacheFileAutoLock lock(this);
|
||||
|
||||
LOG(("CacheFile::RemoveChunk() [this=%p, chunk=%p, idx=%u]",
|
||||
LOG(("CacheFile::DeactivateChunk() [this=%p, chunk=%p, idx=%u]",
|
||||
this, aChunk, aChunk->Index()));
|
||||
|
||||
MOZ_ASSERT(mReady);
|
||||
@ -1317,8 +1319,8 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
(!mHandle && !mMemoryOnly && mOpeningFile));
|
||||
|
||||
if (aChunk->mRefCnt != 2) {
|
||||
LOG(("CacheFile::RemoveChunk() - Chunk is still used [this=%p, chunk=%p, "
|
||||
"refcnt=%d]", this, aChunk, aChunk->mRefCnt.get()));
|
||||
LOG(("CacheFile::DeactivateChunk() - Chunk is still used [this=%p, "
|
||||
"chunk=%p, refcnt=%d]", this, aChunk, aChunk->mRefCnt.get()));
|
||||
|
||||
// somebody got the reference before the lock was acquired
|
||||
return NS_OK;
|
||||
@ -1340,7 +1342,7 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
|
||||
if (NS_FAILED(mStatus)) {
|
||||
// Don't write any chunk to disk since this entry will be doomed
|
||||
LOG(("CacheFile::RemoveChunk() - Releasing chunk because of status "
|
||||
LOG(("CacheFile::DeactivateChunk() - Releasing chunk because of status "
|
||||
"[this=%p, chunk=%p, mStatus=0x%08x]", this, chunk.get(), mStatus));
|
||||
|
||||
RemoveChunkInternal(chunk, false);
|
||||
@ -1348,14 +1350,14 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
}
|
||||
|
||||
if (chunk->IsDirty() && !mMemoryOnly && !mOpeningFile) {
|
||||
LOG(("CacheFile::RemoveChunk() - Writing dirty chunk to the disk "
|
||||
LOG(("CacheFile::DeactivateChunk() - Writing dirty chunk to the disk "
|
||||
"[this=%p]", this));
|
||||
|
||||
mDataIsDirty = true;
|
||||
|
||||
rv = chunk->Write(mHandle, this);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("CacheFile::RemoveChunk() - CacheFileChunk::Write() failed "
|
||||
LOG(("CacheFile::DeactivateChunk() - CacheFileChunk::Write() failed "
|
||||
"synchronously. Removing it. [this=%p, chunk=%p, rv=0x%08x]",
|
||||
this, chunk.get(), rv));
|
||||
|
||||
@ -1365,18 +1367,17 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
CacheFileIOManager::DoomFile(mHandle, nullptr);
|
||||
return rv;
|
||||
}
|
||||
else {
|
||||
// Chunk will be removed in OnChunkWritten if it is still unused
|
||||
|
||||
// chunk needs to be released under the lock to be able to rely on
|
||||
// CacheFileChunk::mRefCnt in CacheFile::OnChunkWritten()
|
||||
chunk = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
// Chunk will be removed in OnChunkWritten if it is still unused
|
||||
|
||||
// chunk needs to be released under the lock to be able to rely on
|
||||
// CacheFileChunk::mRefCnt in CacheFile::OnChunkWritten()
|
||||
chunk = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool keepChunk = ShouldCacheChunk(aChunk->Index());
|
||||
LOG(("CacheFile::RemoveChunk() - %s unused chunk [this=%p, chunk=%p]",
|
||||
LOG(("CacheFile::DeactivateChunk() - %s unused chunk [this=%p, chunk=%p]",
|
||||
keepChunk ? "Caching" : "Releasing", this, chunk.get()));
|
||||
|
||||
RemoveChunkInternal(chunk, keepChunk);
|
||||
@ -1391,7 +1392,9 @@ CacheFile::RemoveChunk(CacheFileChunk *aChunk)
|
||||
void
|
||||
CacheFile::RemoveChunkInternal(CacheFileChunk *aChunk, bool aCacheChunk)
|
||||
{
|
||||
aChunk->mRemovingChunk = true;
|
||||
AssertOwnsLock();
|
||||
|
||||
aChunk->mActiveChunk = false;
|
||||
ReleaseOutsideLock(static_cast<CacheFileChunkListener *>(
|
||||
aChunk->mFile.forget().take()));
|
||||
|
||||
@ -1738,7 +1741,7 @@ CacheFile::WriteAllCachedChunks(const uint32_t& aIdx,
|
||||
|
||||
file->mChunks.Put(aIdx, aChunk);
|
||||
aChunk->mFile = file;
|
||||
aChunk->mRemovingChunk = false;
|
||||
aChunk->mActiveChunk = true;
|
||||
|
||||
MOZ_ASSERT(aChunk->IsReady());
|
||||
|
||||
|
@ -140,7 +140,7 @@ private:
|
||||
bool ShouldCacheChunk(uint32_t aIndex);
|
||||
bool MustKeepCachedChunk(uint32_t aIndex);
|
||||
|
||||
nsresult RemoveChunk(CacheFileChunk *aChunk);
|
||||
nsresult DeactivateChunk(CacheFileChunk *aChunk);
|
||||
void RemoveChunkInternal(CacheFileChunk *aChunk, bool aCacheChunk);
|
||||
|
||||
int64_t BytesFromChunk(uint32_t aIndex);
|
||||
|
@ -45,11 +45,29 @@ protected:
|
||||
nsRefPtr<CacheFileChunk> mChunk;
|
||||
};
|
||||
|
||||
bool
|
||||
CacheFileChunk::DispatchRelease()
|
||||
{
|
||||
if (NS_IsMainThread()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsRefPtr<nsRunnableMethod<CacheFileChunk, MozExternalRefCountType, false> > event =
|
||||
NS_NewNonOwningRunnableMethod(this, &CacheFileChunk::Release);
|
||||
NS_DispatchToMainThread(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NS_IMPL_ADDREF(CacheFileChunk)
|
||||
NS_IMETHODIMP_(MozExternalRefCountType)
|
||||
CacheFileChunk::Release()
|
||||
{
|
||||
if (DispatchRelease()) {
|
||||
// Redispatched to the main thread.
|
||||
return mRefCnt - 1;
|
||||
}
|
||||
|
||||
NS_PRECONDITION(0 != mRefCnt, "dup release");
|
||||
nsrefcnt count = --mRefCnt;
|
||||
NS_LOG_RELEASE(this, count, "CacheFileChunk");
|
||||
@ -60,8 +78,18 @@ CacheFileChunk::Release()
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!mRemovingChunk && count == 1) {
|
||||
mFile->RemoveChunk(this);
|
||||
// We can safely access this chunk after decreasing mRefCnt since we re-post
|
||||
// all calls to Release() happening off the main thread to the main thread.
|
||||
// I.e. no other Release() that would delete the object could be run before
|
||||
// we call CacheFile::DeactivateChunk().
|
||||
//
|
||||
// NOTE: we don't grab the CacheFile's lock, so the chunk might be addrefed
|
||||
// on another thread before CacheFile::DeactivateChunk() grabs the lock on
|
||||
// this thread. To make sure we won't deactivate chunk that was just returned
|
||||
// to a new consumer we check mRefCnt once again in
|
||||
// CacheFile::DeactivateChunk() after we grab the lock.
|
||||
if (mActiveChunk && count == 1) {
|
||||
mFile->DeactivateChunk(this);
|
||||
}
|
||||
|
||||
return count;
|
||||
@ -78,7 +106,7 @@ CacheFileChunk::CacheFileChunk(CacheFile *aFile, uint32_t aIndex)
|
||||
, mState(INITIAL)
|
||||
, mStatus(NS_OK)
|
||||
, mIsDirty(false)
|
||||
, mRemovingChunk(false)
|
||||
, mActiveChunk(false)
|
||||
, mDataSize(0)
|
||||
, mBuf(nullptr)
|
||||
, mBufSize(0)
|
||||
|
@ -68,6 +68,7 @@ class CacheFileChunk : public CacheFileIOListener
|
||||
{
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
bool DispatchRelease();
|
||||
|
||||
CacheFileChunk(CacheFile *aFile, uint32_t aIndex);
|
||||
|
||||
@ -128,7 +129,10 @@ private:
|
||||
EState mState;
|
||||
nsresult mStatus;
|
||||
bool mIsDirty;
|
||||
bool mRemovingChunk;
|
||||
bool mActiveChunk; // Is true iff the chunk is in CacheFile::mChunks.
|
||||
// Adding/removing chunk to/from mChunks as well as
|
||||
// changing this member happens under the CacheFile's
|
||||
// lock.
|
||||
uint32_t mDataSize;
|
||||
|
||||
char *mBuf;
|
||||
|
@ -113,11 +113,6 @@ function PasswordStore(name, engine) {
|
||||
Store.call(this, name, engine);
|
||||
this._nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
|
||||
return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.mozIStorageConnection);
|
||||
});
|
||||
}
|
||||
PasswordStore.prototype = {
|
||||
__proto__: Store.prototype,
|
||||
@ -162,21 +157,6 @@ PasswordStore.prototype = {
|
||||
return null;
|
||||
},
|
||||
|
||||
applyIncomingBatch: function applyIncomingBatch(records) {
|
||||
if (!this.DBConnection) {
|
||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
||||
}
|
||||
|
||||
return Utils.runInTransaction(this.DBConnection, function() {
|
||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
||||
}, this);
|
||||
},
|
||||
|
||||
applyIncoming: function applyIncoming(record) {
|
||||
Store.prototype.applyIncoming.call(this, record);
|
||||
this._sleep(0); // Yield back to main thread after synchronous operation.
|
||||
},
|
||||
|
||||
getAllIDs: function PasswordStore__getAllIDs() {
|
||||
let items = {};
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
|
@ -150,22 +150,6 @@ this.Utils = {
|
||||
};
|
||||
},
|
||||
|
||||
runInTransaction: function(db, callback, thisObj) {
|
||||
let hasTransaction = false;
|
||||
try {
|
||||
db.beginTransaction();
|
||||
hasTransaction = true;
|
||||
} catch(e) { /* om nom nom exceptions */ }
|
||||
|
||||
try {
|
||||
return callback.call(thisObj);
|
||||
} finally {
|
||||
if (hasTransaction) {
|
||||
db.commitTransaction();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
|
||||
* That makes them 12 characters long with 72 bits of entropy.
|
||||
|
233
toolkit/components/passwordmgr/LoginHelper.jsm
Normal file
233
toolkit/components/passwordmgr/LoginHelper.jsm
Normal file
@ -0,0 +1,233 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*
|
||||
* This JavaScript module exists in order to share code between the different
|
||||
* XPCOM components that constitute the Login Manager, including implementations
|
||||
* of nsILoginManager and nsILoginManagerStorage.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginHelper",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginHelper
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*/
|
||||
this.LoginHelper = {
|
||||
/**
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parsed incorrectly. Reject hostnames that we can't store correctly.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
checkHostnameValue: function (aHostname)
|
||||
{
|
||||
// Nulls are invalid, as they don't round-trip well. Newlines are also
|
||||
// invalid for any field stored as plaintext, and a hostname made of a
|
||||
// single dot cannot be stored in the legacy format.
|
||||
if (aHostname == "." ||
|
||||
aHostname.indexOf("\r") != -1 ||
|
||||
aHostname.indexOf("\n") != -1 ||
|
||||
aHostname.indexOf("\0") != -1) {
|
||||
throw "Invalid hostname";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parsed incorrectly. Reject logins that we can't store correctly.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
checkLoginValues: function (aLogin)
|
||||
{
|
||||
function badCharacterPresent(l, c)
|
||||
{
|
||||
return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
|
||||
(l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
|
||||
l.hostname.indexOf(c) != -1 ||
|
||||
l.usernameField.indexOf(c) != -1 ||
|
||||
l.passwordField.indexOf(c) != -1);
|
||||
}
|
||||
|
||||
// Nulls are invalid, as they don't round-trip well.
|
||||
// Mostly not a formatting problem, although ".\0" can be quirky.
|
||||
if (badCharacterPresent(aLogin, "\0")) {
|
||||
throw "login values can't contain nulls";
|
||||
}
|
||||
|
||||
// In theory these nulls should just be rolled up into the encrypted
|
||||
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
|
||||
// nulls cause truncation. Check for them here just to avoid
|
||||
// unexpected round-trip surprises.
|
||||
if (aLogin.username.indexOf("\0") != -1 ||
|
||||
aLogin.password.indexOf("\0") != -1) {
|
||||
throw "login values can't contain nulls";
|
||||
}
|
||||
|
||||
// Newlines are invalid for any field stored as plaintext.
|
||||
if (badCharacterPresent(aLogin, "\r") ||
|
||||
badCharacterPresent(aLogin, "\n")) {
|
||||
throw "login values can't contain newlines";
|
||||
}
|
||||
|
||||
// A line with just a "." can have special meaning.
|
||||
if (aLogin.usernameField == "." ||
|
||||
aLogin.formSubmitURL == ".") {
|
||||
throw "login values can't be periods";
|
||||
}
|
||||
|
||||
// A hostname with "\ \(" won't roundtrip.
|
||||
// eg host="foo (", realm="bar" --> "foo ( (bar)"
|
||||
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
|
||||
if (aLogin.hostname.indexOf(" (") != -1) {
|
||||
throw "bad parens in hostname";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new login object that results by modifying the given object with
|
||||
* the provided data.
|
||||
*
|
||||
* @param aOldStoredLogin
|
||||
* Existing nsILoginInfo object to modify.
|
||||
* @param aNewLoginData
|
||||
* The new login values, either as nsILoginInfo or nsIProperyBag.
|
||||
*
|
||||
* @return The newly created nsILoginInfo object.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
buildModifiedLogin: function (aOldStoredLogin, aNewLoginData)
|
||||
{
|
||||
function bagHasProperty(aPropName)
|
||||
{
|
||||
try {
|
||||
aNewLoginData.getProperty(aPropName);
|
||||
return true;
|
||||
} catch (ex) { }
|
||||
return false;
|
||||
}
|
||||
|
||||
aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
let newLogin;
|
||||
if (aNewLoginData instanceof Ci.nsILoginInfo) {
|
||||
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
||||
// with the replacement nsILoginInfo data from the new login.
|
||||
newLogin = aOldStoredLogin.clone();
|
||||
newLogin.init(aNewLoginData.hostname,
|
||||
aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
|
||||
aNewLoginData.username, aNewLoginData.password,
|
||||
aNewLoginData.usernameField, aNewLoginData.passwordField);
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
if (newLogin.password != aOldStoredLogin.password) {
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
} else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
|
||||
// Clone the existing login, along with all its properties.
|
||||
newLogin = aOldStoredLogin.clone();
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
// (Done before the main property updates, lest the caller be
|
||||
// explicitly updating both .password and .timePasswordChanged)
|
||||
if (bagHasProperty("password")) {
|
||||
let newPassword = aNewLoginData.getProperty("password");
|
||||
if (newPassword != aOldStoredLogin.password) {
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
let propEnum = aNewLoginData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
switch (prop.name) {
|
||||
// nsILoginInfo
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "formSubmitURL":
|
||||
case "username":
|
||||
case "password":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
// nsILoginMetaInfo
|
||||
case "guid":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
newLogin[prop.name] = prop.value;
|
||||
break;
|
||||
|
||||
// Fake property, allows easy incrementing.
|
||||
case "timesUsedIncrement":
|
||||
newLogin.timesUsed += prop.value;
|
||||
break;
|
||||
|
||||
// Fail if caller requests setting an unknown property.
|
||||
default:
|
||||
throw "Unexpected propertybag item: " + prop.name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw "newLoginData needs an expected interface!";
|
||||
}
|
||||
|
||||
// Sanity check the login
|
||||
if (newLogin.hostname == null || newLogin.hostname.length == 0) {
|
||||
throw "Can't add a login with a null or empty hostname.";
|
||||
}
|
||||
|
||||
// For logins w/o a username, set to "", not null.
|
||||
if (newLogin.username == null) {
|
||||
throw "Can't add a login with a null username.";
|
||||
}
|
||||
|
||||
if (newLogin.password == null || newLogin.password.length == 0) {
|
||||
throw "Can't add a login with a null or empty password.";
|
||||
}
|
||||
|
||||
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
||||
// We have a form submit URL. Can't have a HTTP realm.
|
||||
if (newLogin.httpRealm != null) {
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
}
|
||||
} else if (newLogin.httpRealm) {
|
||||
// We have a HTTP realm. Can't have a form submit URL.
|
||||
if (newLogin.formSubmitURL != null) {
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
}
|
||||
} else {
|
||||
// Need one or the other!
|
||||
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
||||
}
|
||||
|
||||
// Throws if there are bogus values.
|
||||
this.checkLoginValues(newLogin);
|
||||
|
||||
return newLogin;
|
||||
},
|
||||
};
|
180
toolkit/components/passwordmgr/LoginImport.jsm
Normal file
180
toolkit/components/passwordmgr/LoginImport.jsm
Normal file
@ -0,0 +1,180 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data from the
|
||||
* previous SQLite storage format.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginImport",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginImport
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data from the
|
||||
* previous SQLite storage format.
|
||||
*
|
||||
* @param aStore
|
||||
* LoginStore object where imported data will be added.
|
||||
* @param aPath
|
||||
* String containing the file path of the SQLite login database.
|
||||
*/
|
||||
this.LoginImport = function (aStore, aPath)
|
||||
{
|
||||
this.store = aStore;
|
||||
this.path = aPath;
|
||||
}
|
||||
|
||||
this.LoginImport.prototype = {
|
||||
/**
|
||||
* LoginStore object where imported data will be added.
|
||||
*/
|
||||
store: null,
|
||||
|
||||
/**
|
||||
* String containing the file path of the SQLite login database.
|
||||
*/
|
||||
path: null,
|
||||
|
||||
/**
|
||||
* Imports login-related data from the previous SQLite storage format.
|
||||
*/
|
||||
import: Task.async(function* () {
|
||||
// We currently migrate data directly from the database to the JSON store at
|
||||
// first run, then we set a preference to prevent repeating the import.
|
||||
// Thus, merging with existing data is not a use case we support. This
|
||||
// restriction might be removed to support re-importing passwords set by an
|
||||
// old version by flipping the import preference and restarting.
|
||||
if (this.store.data.logins.length > 0 ||
|
||||
this.store.data.disabledHosts.length > 0) {
|
||||
throw new Error("Unable to import saved passwords because some data " +
|
||||
"has already been imported or saved.");
|
||||
}
|
||||
|
||||
// When a timestamp is not specified, we will use the same reference time.
|
||||
let referenceTimeMs = Date.now();
|
||||
|
||||
let connection = yield Sqlite.openConnection({ path: this.path });
|
||||
try {
|
||||
let schemaVersion = yield connection.getSchemaVersion();
|
||||
|
||||
// We support importing database schema versions from 3 onwards.
|
||||
// Version 3 was implemented in bug 316084 (Firefox 3.6, March 2009).
|
||||
// Version 4 was implemented in bug 465636 (Firefox 4, March 2010).
|
||||
// Version 5 was implemented in bug 718817 (Firefox 13, February 2012).
|
||||
if (schemaVersion < 3) {
|
||||
throw new Error("Unable to import saved passwords because " +
|
||||
"the existing profile is too old.");
|
||||
}
|
||||
|
||||
let rows = yield connection.execute("SELECT * FROM moz_logins");
|
||||
for (let row of rows) {
|
||||
try {
|
||||
let hostname = row.getResultByName("hostname");
|
||||
let httpRealm = row.getResultByName("httpRealm");
|
||||
let formSubmitURL = row.getResultByName("formSubmitURL");
|
||||
let usernameField = row.getResultByName("usernameField");
|
||||
let passwordField = row.getResultByName("passwordField");
|
||||
let encryptedUsername = row.getResultByName("encryptedUsername");
|
||||
let encryptedPassword = row.getResultByName("encryptedPassword");
|
||||
|
||||
// The "guid" field was introduced in schema version 2, and the
|
||||
// "enctype" field was introduced in schema version 3. We don't
|
||||
// support upgrading from older versions of the database.
|
||||
let guid = row.getResultByName("guid");
|
||||
let encType = row.getResultByName("encType");
|
||||
|
||||
// The time and count fields were introduced in schema version 4.
|
||||
let timeCreated = null;
|
||||
let timeLastUsed = null;
|
||||
let timePasswordChanged = null;
|
||||
let timesUsed = null;
|
||||
try {
|
||||
timeCreated = row.getResultByName("timeCreated");
|
||||
timeLastUsed = row.getResultByName("timeLastUsed");
|
||||
timePasswordChanged = row.getResultByName("timePasswordChanged");
|
||||
timesUsed = row.getResultByName("timesUsed");
|
||||
} catch (ex) { }
|
||||
|
||||
// These columns may be null either because they were not present in
|
||||
// the database or because the record was created on a new schema
|
||||
// version by an old application version.
|
||||
if (!timeCreated) {
|
||||
timeCreated = referenceTimeMs;
|
||||
}
|
||||
if (!timeLastUsed) {
|
||||
timeLastUsed = referenceTimeMs;
|
||||
}
|
||||
if (!timePasswordChanged) {
|
||||
timePasswordChanged = referenceTimeMs;
|
||||
}
|
||||
if (!timesUsed) {
|
||||
timesUsed = 1;
|
||||
}
|
||||
|
||||
this.store.data.logins.push({
|
||||
id: this.store.data.nextId++,
|
||||
hostname: hostname,
|
||||
httpRealm: httpRealm,
|
||||
formSubmitURL: formSubmitURL,
|
||||
usernameField: usernameField,
|
||||
passwordField: passwordField,
|
||||
encryptedUsername: encryptedUsername,
|
||||
encryptedPassword: encryptedPassword,
|
||||
guid: guid,
|
||||
encType: encType,
|
||||
timeCreated: timeCreated,
|
||||
timeLastUsed: timeLastUsed,
|
||||
timePasswordChanged: timePasswordChanged,
|
||||
timesUsed: timesUsed,
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error importing login: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
rows = yield connection.execute("SELECT * FROM moz_disabledHosts");
|
||||
for (let row of rows) {
|
||||
try {
|
||||
let id = row.getResultByName("id");
|
||||
let hostname = row.getResultByName("hostname");
|
||||
|
||||
this.store.data.disabledHosts.push({
|
||||
id: this.store.data.nextId++,
|
||||
hostname: hostname,
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error importing disabled host: " + ex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
}),
|
||||
};
|
301
toolkit/components/passwordmgr/LoginStore.jsm
Normal file
301
toolkit/components/passwordmgr/LoginStore.jsm
Normal file
@ -0,0 +1,301 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Handles serialization of login-related data and persistence into a file.
|
||||
*
|
||||
* This modules handles the raw data stored in JavaScript serializable objects,
|
||||
* and contains no special validation or query logic, that is handled entirely
|
||||
* by "storage.js" instead.
|
||||
*
|
||||
* The data can be manipulated only after it has been loaded from disk. The
|
||||
* load process can happen asynchronously, through the "load" method, or
|
||||
* synchronously, through "ensureDataReady". After any modification, the
|
||||
* "saveSoon" method must be called to flush the data to disk asynchronously.
|
||||
*
|
||||
* The raw data should be manipulated synchronously, without waiting for the
|
||||
* event loop or for promise resolution, so that the saved file is always
|
||||
* consistent. This synchronous approach also simplifies the query and update
|
||||
* logic. For example, it is possible to find an object and modify it
|
||||
* immediately without caring whether other code modifies it in the meantime.
|
||||
*
|
||||
* An asynchronous shutdown observer makes sure that data is always saved before
|
||||
* the browser is closed. The data cannot be modified during shutdown.
|
||||
*
|
||||
* The file is stored in JSON format, without indentation, using UTF-8 encoding.
|
||||
* With indentation applied, the file would look like this:
|
||||
*
|
||||
* {
|
||||
* "logins": [
|
||||
* {
|
||||
* "id": 2,
|
||||
* "hostname": "http://www.example.com",
|
||||
* "httpRealm": null,
|
||||
* "formSubmitURL": "http://www.example.com/submit-url",
|
||||
* "usernameField": "username_field",
|
||||
* "passwordField": "password_field",
|
||||
* "encryptedUsername": "...",
|
||||
* "encryptedPassword": "...",
|
||||
* "guid": "...",
|
||||
* "encType": 1,
|
||||
* "timeCreated": 1262304000000,
|
||||
* "timeLastUsed": 1262304000000,
|
||||
* "timePasswordChanged": 1262476800000,
|
||||
* "timesUsed": 1
|
||||
* },
|
||||
* {
|
||||
* "id": 4,
|
||||
* (...)
|
||||
* }
|
||||
* ],
|
||||
* "disabledHosts": [
|
||||
* "http://www.example.org",
|
||||
* "http://www.example.net"
|
||||
* ],
|
||||
* "nextId": 10,
|
||||
* "version": 1
|
||||
* }
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginStore",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm")
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
|
||||
return new TextDecoder();
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
|
||||
return new TextEncoder();
|
||||
});
|
||||
|
||||
const FileInputStream =
|
||||
Components.Constructor("@mozilla.org/network/file-input-stream;1",
|
||||
"nsIFileInputStream", "init");
|
||||
|
||||
/**
|
||||
* Delay between a change to the login data and the related save operation.
|
||||
*/
|
||||
const kSaveDelayMs = 1500;
|
||||
|
||||
/**
|
||||
* Current data version assigned by the code that last touched the data.
|
||||
*
|
||||
* This number should be updated only when it is important to understand whether
|
||||
* an old version of the code has touched the data, for example to execute an
|
||||
* update logic. In most cases, this number should not be changed, in
|
||||
* particular when no special one-time update logic is needed.
|
||||
*
|
||||
* For example, this number should NOT be changed when a new optional field is
|
||||
* added to a login entry.
|
||||
*/
|
||||
const kDataVersion = 1;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginStore
|
||||
|
||||
/**
|
||||
* Handles serialization of login-related data and persistence into a file.
|
||||
*
|
||||
* @param aPath
|
||||
* String containing the file path where data should be saved.
|
||||
*/
|
||||
function LoginStore(aPath)
|
||||
{
|
||||
this.path = aPath;
|
||||
|
||||
this._saver = new DeferredTask(() => this.save(), kSaveDelayMs);
|
||||
AsyncShutdown.profileBeforeChange.addBlocker("Login store: writing data",
|
||||
() => this._saver.finalize());
|
||||
}
|
||||
|
||||
LoginStore.prototype = {
|
||||
/**
|
||||
* String containing the file path where data should be saved.
|
||||
*/
|
||||
path: "",
|
||||
|
||||
/**
|
||||
* Serializable object containing the login-related data. This is populated
|
||||
* directly with the data loaded from the file, and is saved without
|
||||
* modifications.
|
||||
*
|
||||
* This contains one property for each list.
|
||||
*/
|
||||
data: null,
|
||||
|
||||
/**
|
||||
* True when data has been loaded.
|
||||
*/
|
||||
dataReady: false,
|
||||
|
||||
/**
|
||||
* Loads persistent data from the file to memory.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
load: function ()
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
try {
|
||||
let bytes = yield OS.File.read(this.path);
|
||||
|
||||
// If synchronous loading happened in the meantime, exit now.
|
||||
if (this.dataReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = JSON.parse(gTextDecoder.decode(bytes));
|
||||
} catch (ex) {
|
||||
// If an exception occurred because the file did not exist, we should
|
||||
// just start with new data. Other errors may indicate that the file is
|
||||
// corrupt, thus we move it to a backup location before allowing it to
|
||||
// be overwritten by an empty file.
|
||||
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
|
||||
Cu.reportError(ex);
|
||||
|
||||
// Move the original file to a backup location, ignoring errors.
|
||||
try {
|
||||
let openInfo = yield OS.File.openUnique(this.path + ".corrupt",
|
||||
{ humanReadable: true });
|
||||
yield openInfo.file.close();
|
||||
yield OS.File.move(this.path, openInfo.path);
|
||||
} catch (e2) {
|
||||
Cu.reportError(e2);
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, initialize a new object to host the data.
|
||||
this.data = {
|
||||
nextId: 1,
|
||||
};
|
||||
}
|
||||
|
||||
this._processLoadedData();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads persistent data from the file to memory, synchronously.
|
||||
*/
|
||||
ensureDataReady: function ()
|
||||
{
|
||||
if (this.dataReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This reads the file and automatically detects the UTF-8 encoding.
|
||||
let inputStream = new FileInputStream(new FileUtils.File(this.path),
|
||||
FileUtils.MODE_RDONLY,
|
||||
FileUtils.PERMS_FILE, 0)
|
||||
try {
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this.data = json.decodeFromStream(inputStream,
|
||||
inputStream.available());
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (ex) {
|
||||
// If an exception occurred because the file did not exist, we should just
|
||||
// start with new data. Other errors may indicate that the file is
|
||||
// corrupt, thus we move it to a backup location before allowing it to be
|
||||
// overwritten by an empty file.
|
||||
if (!(ex instanceof Components.Exception &&
|
||||
ex.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
|
||||
Cu.reportError(ex);
|
||||
// Move the original file to a backup location, ignoring errors.
|
||||
try {
|
||||
let originalFile = new FileUtils.File(this.path);
|
||||
let backupFile = originalFile.clone();
|
||||
backupFile.leafName += ".corrupt";
|
||||
backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE,
|
||||
FileUtils.PERMS_FILE);
|
||||
backupFile.remove(false);
|
||||
originalFile.moveTo(backupFile.parent, backupFile.leafName);
|
||||
} catch (e2) {
|
||||
Cu.reportError(e2);
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, initialize a new object to host the data.
|
||||
this.data = {
|
||||
nextId: 1,
|
||||
};
|
||||
}
|
||||
|
||||
this._processLoadedData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously work on the data just loaded into memory.
|
||||
*/
|
||||
_processLoadedData: function ()
|
||||
{
|
||||
// Create any arrays that are not present in the saved file.
|
||||
if (!this.data.logins) {
|
||||
this.data.logins = [];
|
||||
}
|
||||
if (!this.data.disabledHosts) {
|
||||
this.data.disabledHosts = [];
|
||||
}
|
||||
|
||||
// Indicate that the current version of the code has touched the file.
|
||||
this.data.version = kDataVersion;
|
||||
|
||||
this.dataReady = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the data changed, this triggers asynchronous serialization.
|
||||
*/
|
||||
saveSoon: function () this._saver.arm(),
|
||||
|
||||
/**
|
||||
* DeferredTask that handles the save operation.
|
||||
*/
|
||||
_saver: null,
|
||||
|
||||
/**
|
||||
* Saves persistent data from memory to the file.
|
||||
*
|
||||
* If an error occurs, the previous file is not deleted.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
save: function ()
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
// Create or overwrite the file.
|
||||
let bytes = gTextEncoder.encode(JSON.stringify(this.data));
|
||||
yield OS.File.writeAtomic(this.path, bytes,
|
||||
{ tmpPath: this.path + ".tmp" });
|
||||
}.bind(this));
|
||||
},
|
||||
};
|
@ -23,18 +23,31 @@ XPIDL_MODULE = 'loginmgr'
|
||||
EXTRA_COMPONENTS += [
|
||||
'crypto-SDR.js',
|
||||
'nsLoginInfo.js',
|
||||
'nsLoginManager.js',
|
||||
'nsLoginManagerPrompter.js',
|
||||
'passwordmgr.manifest',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'storage-mozStorage.js',
|
||||
'nsLoginManager.js',
|
||||
'passwordmgr.manifest',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'InsecurePasswordUtils.jsm',
|
||||
'LoginHelper.jsm',
|
||||
'LoginManagerContent.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['OS_TARGET'] == 'Android':
|
||||
EXTRA_COMPONENTS += [
|
||||
'storage-mozStorage.js',
|
||||
]
|
||||
else:
|
||||
EXTRA_COMPONENTS += [
|
||||
'storage-json.js',
|
||||
]
|
||||
EXTRA_JS_MODULES += [
|
||||
'LoginImport.jsm',
|
||||
'LoginStore.jsm',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
@ -125,7 +125,11 @@ LoginManager.prototype = {
|
||||
|
||||
|
||||
_initStorage : function () {
|
||||
#ifdef ANDROID
|
||||
var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
|
||||
#else
|
||||
var contractID = "@mozilla.org/login-manager/storage/json;1";
|
||||
#endif
|
||||
try {
|
||||
var catMan = Cc["@mozilla.org/categorymanager;1"].
|
||||
getService(Ci.nsICategoryManager);
|
||||
|
@ -6,7 +6,12 @@ component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
|
||||
contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
|
||||
component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
|
||||
contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
|
||||
#ifdef ANDROID
|
||||
component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
|
||||
contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
|
||||
#else
|
||||
component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
|
||||
contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
|
||||
#endif
|
||||
component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
|
||||
contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}
|
||||
|
654
toolkit/components/passwordmgr/storage-json.js
Normal file
654
toolkit/components/passwordmgr/storage-json.js
Normal file
@ -0,0 +1,654 @@
|
||||
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* nsILoginManagerStorage implementation for the JSON back-end.
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||
"resource://gre/modules/LoginImport.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginManagerStorage_json
|
||||
|
||||
this.LoginManagerStorage_json = function () {}
|
||||
|
||||
this.LoginManagerStorage_json.prototype = {
|
||||
classID : Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
|
||||
|
||||
__crypto : null, // nsILoginManagerCrypto service
|
||||
get _crypto() {
|
||||
if (!this.__crypto)
|
||||
this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
|
||||
getService(Ci.nsILoginManagerCrypto);
|
||||
return this.__crypto;
|
||||
},
|
||||
|
||||
/*
|
||||
* log
|
||||
*
|
||||
* Internal function for logging debug messages to the Error Console.
|
||||
*/
|
||||
log : function (message) {
|
||||
if (!this._debug)
|
||||
return;
|
||||
dump("PwMgr json: " + message + "\n");
|
||||
Services.console.logStringMessage("PwMgr json: " + message);
|
||||
},
|
||||
_debug : false,
|
||||
|
||||
|
||||
/*
|
||||
* initialize
|
||||
*
|
||||
*/
|
||||
initialize : function () {
|
||||
this._debug = Services.prefs.getBoolPref("signon.debug");
|
||||
|
||||
try {
|
||||
// Force initialization of the crypto module.
|
||||
// See bug 717490 comment 17.
|
||||
this._crypto;
|
||||
|
||||
// Set the reference to LoginStore synchronously.
|
||||
let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
|
||||
"logins.json");
|
||||
this._store = new LoginStore(jsonPath);
|
||||
|
||||
return Task.spawn(function () {
|
||||
// Load the data asynchronously.
|
||||
this.log("Opening database at " + this._store.path);
|
||||
yield this._store.load();
|
||||
|
||||
// The import from previous versions operates the first time
|
||||
// that this built-in storage back-end is used. This may be
|
||||
// later than expected, in case add-ons have registered an
|
||||
// alternate storage that disabled the default one.
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
// If the preference does not exist, we need to import.
|
||||
}
|
||||
|
||||
// Import only happens asynchronously.
|
||||
let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
|
||||
"signons.sqlite");
|
||||
if (yield OS.File.exists(sqlitePath)) {
|
||||
let loginImport = new LoginImport(this._store, sqlitePath);
|
||||
// Failures during import, for example due to a corrupt
|
||||
// file or a schema version that is too old, will not
|
||||
// prevent us from marking the operation as completed.
|
||||
// At the next startup, we will not try the import again.
|
||||
yield loginImport.import().catch(Cu.reportError);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
|
||||
// We won't attempt import again on next startup.
|
||||
Services.prefs.setBoolPref("signon.importedFromSqlite", true);
|
||||
}.bind(this)).catch(Cu.reportError);
|
||||
} catch (e) {
|
||||
this.log("Initialization failed: " + e);
|
||||
throw "Initialization failed";
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* terminate
|
||||
*
|
||||
* Internal method used by regression tests only. It is called before
|
||||
* replacing this storage module with a new instance.
|
||||
*/
|
||||
terminate : function () {
|
||||
this._store._saver.disarm();
|
||||
return this._store.save();
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* addLogin
|
||||
*
|
||||
*/
|
||||
addLogin : function (login) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let encUsername, encPassword;
|
||||
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkLoginValues(login);
|
||||
|
||||
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
||||
|
||||
// Clone the login, so we don't modify the caller's object.
|
||||
let loginClone = login.clone();
|
||||
|
||||
// Initialize the nsILoginMetaInfo fields, unless the caller gave us values
|
||||
loginClone.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
if (loginClone.guid) {
|
||||
if (!this._isGuidUnique(loginClone.guid))
|
||||
throw "specified GUID already exists";
|
||||
} else {
|
||||
loginClone.guid = gUUIDGenerator.generateUUID().toString();
|
||||
}
|
||||
|
||||
// Set timestamps
|
||||
let currentTime = Date.now();
|
||||
if (!loginClone.timeCreated)
|
||||
loginClone.timeCreated = currentTime;
|
||||
if (!loginClone.timeLastUsed)
|
||||
loginClone.timeLastUsed = currentTime;
|
||||
if (!loginClone.timePasswordChanged)
|
||||
loginClone.timePasswordChanged = currentTime;
|
||||
if (!loginClone.timesUsed)
|
||||
loginClone.timesUsed = 1;
|
||||
|
||||
this._store.data.logins.push({
|
||||
id: this._store.data.nextId++,
|
||||
hostname: loginClone.hostname,
|
||||
httpRealm: loginClone.httpRealm,
|
||||
formSubmitURL: loginClone.formSubmitURL,
|
||||
usernameField: loginClone.usernameField,
|
||||
passwordField: loginClone.passwordField,
|
||||
encryptedUsername: encUsername,
|
||||
encryptedPassword: encPassword,
|
||||
guid: loginClone.guid,
|
||||
encType: encType,
|
||||
timeCreated: loginClone.timeCreated,
|
||||
timeLastUsed: loginClone.timeLastUsed,
|
||||
timePasswordChanged: loginClone.timePasswordChanged,
|
||||
timesUsed: loginClone.timesUsed
|
||||
});
|
||||
this._store.saveSoon();
|
||||
|
||||
// Send a notification that a login was added.
|
||||
this._sendNotification("addLogin", loginClone);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* removeLogin
|
||||
*
|
||||
*/
|
||||
removeLogin : function (login) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let [idToDelete, storedLogin] = this._getIdForLogin(login);
|
||||
if (!idToDelete)
|
||||
throw "No matching logins";
|
||||
|
||||
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
|
||||
if (foundIndex != -1) {
|
||||
this._store.data.logins.splice(foundIndex, 1);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
|
||||
this._sendNotification("removeLogin", storedLogin);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* modifyLogin
|
||||
*
|
||||
*/
|
||||
modifyLogin : function (oldLogin, newLoginData) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||
if (!idToModify)
|
||||
throw "No matching logins";
|
||||
|
||||
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
|
||||
|
||||
// Check if the new GUID is duplicate.
|
||||
if (newLogin.guid != oldStoredLogin.guid &&
|
||||
!this._isGuidUnique(newLogin.guid))
|
||||
{
|
||||
throw "specified GUID already exists";
|
||||
}
|
||||
|
||||
// Look for an existing entry in case key properties changed.
|
||||
if (!newLogin.matches(oldLogin, true)) {
|
||||
let logins = this.findLogins({}, newLogin.hostname,
|
||||
newLogin.formSubmitURL,
|
||||
newLogin.httpRealm);
|
||||
|
||||
if (logins.some(login => newLogin.matches(login, true)))
|
||||
throw "This login already exists.";
|
||||
}
|
||||
|
||||
// Get the encrypted value of the username and password.
|
||||
let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
|
||||
|
||||
for (let loginItem of this._store.data.logins) {
|
||||
if (loginItem.id == idToModify) {
|
||||
loginItem.hostname = newLogin.hostname;
|
||||
loginItem.httpRealm = newLogin.httpRealm;
|
||||
loginItem.formSubmitURL = newLogin.formSubmitURL;
|
||||
loginItem.usernameField = newLogin.usernameField;
|
||||
loginItem.passwordField = newLogin.passwordField;
|
||||
loginItem.encryptedUsername = encUsername;
|
||||
loginItem.encryptedPassword = encPassword;
|
||||
loginItem.guid = newLogin.guid;
|
||||
loginItem.encType = encType;
|
||||
loginItem.timeCreated = newLogin.timeCreated;
|
||||
loginItem.timeLastUsed = newLogin.timeLastUsed;
|
||||
loginItem.timePasswordChanged = newLogin.timePasswordChanged;
|
||||
loginItem.timesUsed = newLogin.timesUsed;
|
||||
this._store.saveSoon();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getAllLogins
|
||||
*
|
||||
* Returns an array of nsILoginInfo.
|
||||
*/
|
||||
getAllLogins : function (count) {
|
||||
let [logins, ids] = this._searchLogins({});
|
||||
|
||||
// decrypt entries for caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
||||
this.log("_getAllLogins: returning " + logins.length + " logins.");
|
||||
if (count)
|
||||
count.value = logins.length; // needed for XPCOM
|
||||
return logins;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* searchLogins
|
||||
*
|
||||
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
|
||||
* JavaScript object and decrypt the results.
|
||||
*
|
||||
* Returns an array of decrypted nsILoginInfo.
|
||||
*/
|
||||
searchLogins : function(count, matchData) {
|
||||
let realMatchData = {};
|
||||
// Convert nsIPropertyBag to normal JS object
|
||||
let propEnum = matchData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
realMatchData[prop.name] = prop.value;
|
||||
}
|
||||
|
||||
let [logins, ids] = this._searchLogins(realMatchData);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
||||
count.value = logins.length; // needed for XPCOM
|
||||
return logins;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _searchLogins
|
||||
*
|
||||
* Private method to perform arbitrary searches on any field. Decryption is
|
||||
* left to the caller.
|
||||
*
|
||||
* Returns [logins, ids] for logins that match the arguments, where logins
|
||||
* is an array of encrypted nsLoginInfo and ids is an array of associated
|
||||
* ids in the database.
|
||||
*/
|
||||
_searchLogins : function (matchData) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let conditions = [];
|
||||
|
||||
function match(aLogin) {
|
||||
for (let field in matchData) {
|
||||
let value = matchData[field];
|
||||
switch (field) {
|
||||
// Historical compatibility requires this special case
|
||||
case "formSubmitURL":
|
||||
if (value != null) {
|
||||
if (aLogin.formSubmitURL != "" && aLogin.formSubmitURL != value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Normal cases.
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "id":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
case "encryptedUsername":
|
||||
case "encryptedPassword":
|
||||
case "guid":
|
||||
case "encType":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
if (value == null && aLogin[field]) {
|
||||
return false;
|
||||
} else if (aLogin[field] != value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
// Fail if caller requests an unknown property.
|
||||
default:
|
||||
throw "Unexpected field: " + field;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let foundLogins = [], foundIds = [];
|
||||
for (let loginItem of this._store.data.logins) {
|
||||
if (match(loginItem)) {
|
||||
// Create the new nsLoginInfo object, push to array
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
||||
createInstance(Ci.nsILoginInfo);
|
||||
login.init(loginItem.hostname, loginItem.formSubmitURL,
|
||||
loginItem.httpRealm, loginItem.encryptedUsername,
|
||||
loginItem.encryptedPassword, loginItem.usernameField,
|
||||
loginItem.passwordField);
|
||||
// set nsILoginMetaInfo values
|
||||
login.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
login.guid = loginItem.guid;
|
||||
login.timeCreated = loginItem.timeCreated;
|
||||
login.timeLastUsed = loginItem.timeLastUsed;
|
||||
login.timePasswordChanged = loginItem.timePasswordChanged;
|
||||
login.timesUsed = loginItem.timesUsed;
|
||||
foundLogins.push(login);
|
||||
foundIds.push(loginItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.log("_searchLogins: returning " + foundLogins.length + " logins");
|
||||
return [foundLogins, foundIds];
|
||||
},
|
||||
|
||||
/*
|
||||
* removeAllLogins
|
||||
*
|
||||
* Removes all logins from storage.
|
||||
*
|
||||
* Disabled hosts are kept, as one presumably doesn't want to erase those.
|
||||
*/
|
||||
removeAllLogins : function () {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
this.log("Removing all logins");
|
||||
this._store.data.logins = [];
|
||||
this._store.saveSoon();
|
||||
|
||||
this._sendNotification("removeAllLogins", null);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getAllDisabledHosts
|
||||
*
|
||||
*/
|
||||
getAllDisabledHosts : function (count) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let disabledHosts = this._store.data.disabledHosts.slice(0);
|
||||
|
||||
this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
|
||||
if (count)
|
||||
count.value = disabledHosts.length; // needed for XPCOM
|
||||
return disabledHosts;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getLoginSavingEnabled
|
||||
*
|
||||
*/
|
||||
getLoginSavingEnabled : function (hostname) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
this.log("Getting login saving is enabled for " + hostname);
|
||||
return this._store.data.disabledHosts.indexOf(hostname) == -1;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* setLoginSavingEnabled
|
||||
*
|
||||
*/
|
||||
setLoginSavingEnabled : function (hostname, enabled) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkHostnameValue(hostname);
|
||||
|
||||
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||
let foundIndex = this._store.data.disabledHosts.indexOf(hostname);
|
||||
if (enabled) {
|
||||
if (foundIndex != -1) {
|
||||
this._store.data.disabledHosts.splice(foundIndex, 1);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
} else {
|
||||
if (foundIndex == -1) {
|
||||
this._store.data.disabledHosts.push(hostname);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
}
|
||||
|
||||
this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* findLogins
|
||||
*
|
||||
*/
|
||||
findLogins : function (count, hostname, formSubmitURL, httpRealm) {
|
||||
let loginData = {
|
||||
hostname: hostname,
|
||||
formSubmitURL: formSubmitURL,
|
||||
httpRealm: httpRealm
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
||||
this.log("_findLogins: returning " + logins.length + " logins");
|
||||
count.value = logins.length; // needed for XPCOM
|
||||
return logins;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* countLogins
|
||||
*
|
||||
*/
|
||||
countLogins : function (hostname, formSubmitURL, httpRealm) {
|
||||
let count = {};
|
||||
let loginData = {
|
||||
hostname: hostname,
|
||||
formSubmitURL: formSubmitURL,
|
||||
httpRealm: httpRealm
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
this.log("_countLogins: counted logins: " + logins.length);
|
||||
return logins.length;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* uiBusy
|
||||
*/
|
||||
get uiBusy() {
|
||||
return this._crypto.uiBusy;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* isLoggedIn
|
||||
*/
|
||||
get isLoggedIn() {
|
||||
return this._crypto.isLoggedIn;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _sendNotification
|
||||
*
|
||||
* Send a notification when stored data is changed.
|
||||
*/
|
||||
_sendNotification : function (changeType, data) {
|
||||
let dataObject = data;
|
||||
// Can't pass a raw JS string or array though notifyObservers(). :-(
|
||||
if (data instanceof Array) {
|
||||
dataObject = Cc["@mozilla.org/array;1"].
|
||||
createInstance(Ci.nsIMutableArray);
|
||||
for (let i = 0; i < data.length; i++)
|
||||
dataObject.appendElement(data[i], false);
|
||||
} else if (typeof(data) == "string") {
|
||||
dataObject = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
dataObject.data = data;
|
||||
}
|
||||
Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _getIdForLogin
|
||||
*
|
||||
* Returns an array with two items: [id, login]. If the login was not
|
||||
* found, both items will be null. The returned login contains the actual
|
||||
* stored login (useful for looking at the actual nsILoginMetaInfo values).
|
||||
*/
|
||||
_getIdForLogin : function (login) {
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (login[field] != '')
|
||||
matchData[field] = login[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
let id = null;
|
||||
let foundLogin = null;
|
||||
|
||||
// The specified login isn't encrypted, so we need to ensure
|
||||
// the logins we're comparing with are decrypted. We decrypt one entry
|
||||
// at a time, lest _decryptLogins return fewer entries and screw up
|
||||
// indices between the two.
|
||||
for (let i = 0; i < logins.length; i++) {
|
||||
let [decryptedLogin] = this._decryptLogins([logins[i]]);
|
||||
|
||||
if (!decryptedLogin || !decryptedLogin.equals(login))
|
||||
continue;
|
||||
|
||||
// We've found a match, set id and break
|
||||
foundLogin = decryptedLogin;
|
||||
id = ids[i];
|
||||
break;
|
||||
}
|
||||
|
||||
return [id, foundLogin];
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _isGuidUnique
|
||||
*
|
||||
* Checks to see if the specified GUID already exists.
|
||||
*/
|
||||
_isGuidUnique : function (guid) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
return this._store.data.logins.every(l => l.guid != guid);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _encryptLogin
|
||||
*
|
||||
* Returns the encrypted username, password, and encrypton type for the specified
|
||||
* login. Can throw if the user cancels a master password entry.
|
||||
*/
|
||||
_encryptLogin : function (login) {
|
||||
let encUsername = this._crypto.encrypt(login.username);
|
||||
let encPassword = this._crypto.encrypt(login.password);
|
||||
let encType = this._crypto.defaultEncType;
|
||||
|
||||
return [encUsername, encPassword, encType];
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _decryptLogins
|
||||
*
|
||||
* Decrypts username and password fields in the provided array of
|
||||
* logins.
|
||||
*
|
||||
* The entries specified by the array will be decrypted, if possible.
|
||||
* An array of successfully decrypted logins will be returned. The return
|
||||
* value should be given to external callers (since still-encrypted
|
||||
* entries are useless), whereas internal callers generally don't want
|
||||
* to lose unencrypted entries (eg, because the user clicked Cancel
|
||||
* instead of entering their master password)
|
||||
*/
|
||||
_decryptLogins : function (logins) {
|
||||
let result = [];
|
||||
|
||||
for each (let login in logins) {
|
||||
try {
|
||||
login.username = this._crypto.decrypt(login.username);
|
||||
login.password = this._crypto.decrypt(login.password);
|
||||
} catch (e) {
|
||||
// If decryption failed (corrupt entry?), just skip it.
|
||||
// Rethrow other errors (like canceling entry of a master pw)
|
||||
if (e.result == Cr.NS_ERROR_FAILURE)
|
||||
continue;
|
||||
throw e;
|
||||
}
|
||||
result.push(login);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);
|
@ -13,9 +13,10 @@ const DB_VERSION = 5; // The database schema version
|
||||
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
|
||||
/**
|
||||
* Object that manages a database transaction properly so consumers don't have
|
||||
@ -243,7 +244,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
let encUsername, encPassword;
|
||||
|
||||
// Throws if there are bogus values.
|
||||
this._checkLoginValues(login);
|
||||
LoginHelper.checkLoginValues(login);
|
||||
|
||||
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
||||
|
||||
@ -355,109 +356,16 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||
if (!idToModify)
|
||||
throw "No matching logins";
|
||||
oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
let newLogin;
|
||||
if (newLoginData instanceof Ci.nsILoginInfo) {
|
||||
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
||||
// with the replacement nsILoginInfo data from the new login.
|
||||
newLogin = oldStoredLogin.clone();
|
||||
newLogin.init(newLoginData.hostname,
|
||||
newLoginData.formSubmitURL, newLoginData.httpRealm,
|
||||
newLoginData.username, newLoginData.password,
|
||||
newLoginData.usernameField, newLoginData.passwordField);
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
if (newLogin.password != oldLogin.password)
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
} else if (newLoginData instanceof Ci.nsIPropertyBag) {
|
||||
function _bagHasProperty(aPropName) {
|
||||
try {
|
||||
newLoginData.getProperty(aPropName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clone the existing login, along with all its properties.
|
||||
newLogin = oldStoredLogin.clone();
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
// (Done before the main property updates, lest the caller be
|
||||
// explicitly updating both .password and .timePasswordChanged)
|
||||
if (_bagHasProperty("password")) {
|
||||
let newPassword = newLoginData.getProperty("password");
|
||||
if (newPassword != oldLogin.password)
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
|
||||
let propEnum = newLoginData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
switch (prop.name) {
|
||||
// nsILoginInfo properties...
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "formSubmitURL":
|
||||
case "username":
|
||||
case "password":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
// nsILoginMetaInfo properties...
|
||||
case "guid":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
newLogin[prop.name] = prop.value;
|
||||
if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid))
|
||||
throw "specified GUID already exists";
|
||||
break;
|
||||
|
||||
// Fake property, allows easy incrementing.
|
||||
case "timesUsedIncrement":
|
||||
newLogin.timesUsed += prop.value;
|
||||
break;
|
||||
|
||||
// Fail if caller requests setting an unknown property.
|
||||
default:
|
||||
throw "Unexpected propertybag item: " + prop.name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw "newLoginData needs an expected interface!";
|
||||
// Check if the new GUID is duplicate.
|
||||
if (newLogin.guid != oldStoredLogin.guid &&
|
||||
!this._isGuidUnique(newLogin.guid))
|
||||
{
|
||||
throw "specified GUID already exists";
|
||||
}
|
||||
|
||||
// Sanity check the login
|
||||
if (newLogin.hostname == null || newLogin.hostname.length == 0)
|
||||
throw "Can't add a login with a null or empty hostname.";
|
||||
|
||||
// For logins w/o a username, set to "", not null.
|
||||
if (newLogin.username == null)
|
||||
throw "Can't add a login with a null username.";
|
||||
|
||||
if (newLogin.password == null || newLogin.password.length == 0)
|
||||
throw "Can't add a login with a null or empty password.";
|
||||
|
||||
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
||||
// We have a form submit URL. Can't have a HTTP realm.
|
||||
if (newLogin.httpRealm != null)
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
} else if (newLogin.httpRealm) {
|
||||
// We have a HTTP realm. Can't have a form submit URL.
|
||||
if (newLogin.formSubmitURL != null)
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
} else {
|
||||
// Need one or the other!
|
||||
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
||||
}
|
||||
|
||||
// Throws if there are bogus values.
|
||||
this._checkLoginValues(newLogin);
|
||||
|
||||
// Look for an existing entry in case key properties changed.
|
||||
if (!newLogin.matches(oldLogin, true)) {
|
||||
let logins = this.findLogins({}, newLogin.hostname,
|
||||
@ -665,7 +573,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
*
|
||||
*/
|
||||
storeDeletedLogin : function(aLogin) {
|
||||
#ifdef ANDROID
|
||||
let stmt = null;
|
||||
try {
|
||||
this.log("Storing " + aLogin.guid + " in deleted passwords\n");
|
||||
@ -680,7 +587,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
if (stmt)
|
||||
stmt.reset();
|
||||
}
|
||||
#endif
|
||||
},
|
||||
|
||||
|
||||
@ -714,7 +620,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
}
|
||||
|
||||
this._sendNotification("removeAllLogins", null);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
@ -747,7 +653,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
*/
|
||||
setLoginSavingEnabled : function (hostname, enabled) {
|
||||
// Throws if there are bogus values.
|
||||
this._checkHostnameValue(hostname);
|
||||
LoginHelper.checkHostnameValue(hostname);
|
||||
|
||||
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||
let query;
|
||||
@ -788,8 +694,8 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
@ -978,70 +884,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _checkLoginValues
|
||||
*
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parse incorrectly. Reject logins that we can't store correctly.
|
||||
*/
|
||||
_checkLoginValues : function (aLogin) {
|
||||
function badCharacterPresent(l, c) {
|
||||
return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
|
||||
(l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
|
||||
l.hostname.indexOf(c) != -1 ||
|
||||
l.usernameField.indexOf(c) != -1 ||
|
||||
l.passwordField.indexOf(c) != -1);
|
||||
}
|
||||
|
||||
// Nulls are invalid, as they don't round-trip well.
|
||||
// Mostly not a formatting problem, although ".\0" can be quirky.
|
||||
if (badCharacterPresent(aLogin, "\0"))
|
||||
throw "login values can't contain nulls";
|
||||
|
||||
// In theory these nulls should just be rolled up into the encrypted
|
||||
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
|
||||
// nulls cause truncation. Check for them here just to avoid
|
||||
// unexpected round-trip surprises.
|
||||
if (aLogin.username.indexOf("\0") != -1 ||
|
||||
aLogin.password.indexOf("\0") != -1)
|
||||
throw "login values can't contain nulls";
|
||||
|
||||
// Newlines are invalid for any field stored as plaintext.
|
||||
if (badCharacterPresent(aLogin, "\r") ||
|
||||
badCharacterPresent(aLogin, "\n"))
|
||||
throw "login values can't contain newlines";
|
||||
|
||||
// A line with just a "." can have special meaning.
|
||||
if (aLogin.usernameField == "." ||
|
||||
aLogin.formSubmitURL == ".")
|
||||
throw "login values can't be periods";
|
||||
|
||||
// A hostname with "\ \(" won't roundtrip.
|
||||
// eg host="foo (", realm="bar" --> "foo ( (bar)"
|
||||
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
|
||||
if (aLogin.hostname.indexOf(" (") != -1)
|
||||
throw "bad parens in hostname";
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _checkHostnameValue
|
||||
*
|
||||
* Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
|
||||
* that standard here. Throws on illegal format.
|
||||
*/
|
||||
_checkHostnameValue : function (hostname) {
|
||||
// File format prohibits certain values. Also, nulls
|
||||
// won't round-trip with getAllDisabledHosts().
|
||||
if (hostname == "." ||
|
||||
hostname.indexOf("\r") != -1 ||
|
||||
hostname.indexOf("\n") != -1 ||
|
||||
hostname.indexOf("\0") != -1)
|
||||
throw "Invalid hostname";
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _isGuidUnique
|
||||
*
|
||||
|
@ -17,12 +17,14 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
|
||||
"resource://gre/modules/DownloadPaths.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
|
||||
const LoginInfo =
|
||||
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
@ -43,6 +45,46 @@ function run_test()
|
||||
// Some of these functions are already implemented in other parts of the source
|
||||
// tree, see bug 946708 about sharing more code.
|
||||
|
||||
// While the previous test file should have deleted all the temporary files it
|
||||
// used, on Windows these might still be pending deletion on the physical file
|
||||
// system. Thus, start from a new base number every time, to make a collision
|
||||
// with a file that is still pending deletion highly unlikely.
|
||||
let gFileCounter = Math.floor(Math.random() * 1000000);
|
||||
|
||||
/**
|
||||
* Returns a reference to a temporary file, that is guaranteed not to exist, and
|
||||
* to have never been created before.
|
||||
*
|
||||
* @param aLeafName
|
||||
* Suggested leaf name for the file to be created.
|
||||
*
|
||||
* @return nsIFile pointing to a non-existent file in a temporary directory.
|
||||
*
|
||||
* @note It is not enough to delete the file if it exists, or to delete the file
|
||||
* after calling nsIFile.createUnique, because on Windows the delete
|
||||
* operation in the file system may still be pending, preventing a new
|
||||
* file with the same name to be created.
|
||||
*/
|
||||
function getTempFile(aLeafName)
|
||||
{
|
||||
// Prepend a serial number to the extension in the suggested leaf name.
|
||||
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
|
||||
let leafName = base + "-" + gFileCounter + ext;
|
||||
gFileCounter++;
|
||||
|
||||
// Get a file reference under the temporary directory for this test file.
|
||||
let file = FileUtils.getFile("TmpD", [leafName]);
|
||||
do_check_false(file.exists());
|
||||
|
||||
do_register_cleanup(function () {
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows waiting for an observer notification once.
|
||||
*
|
||||
|
@ -0,0 +1,248 @@
|
||||
/* -*- Mode: C++; tab-width: 8; 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/ */
|
||||
|
||||
/**
|
||||
* Tests the LoginImport object.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||
"resource://gre/modules/LoginImport.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto",
|
||||
"@mozilla.org/login-manager/crypto/SDR;1",
|
||||
"nsILoginManagerCrypto");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
/**
|
||||
* Creates empty login data tables in the given SQLite connection, resembling
|
||||
* the most recent schema version (excluding indices).
|
||||
*/
|
||||
function promiseCreateDatabaseSchema(aConnection)
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
yield aConnection.setSchemaVersion(5);
|
||||
yield aConnection.execute("CREATE TABLE moz_logins (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"hostname TEXT NOT NULL," +
|
||||
"httpRealm TEXT," +
|
||||
"formSubmitURL TEXT," +
|
||||
"usernameField TEXT NOT NULL," +
|
||||
"passwordField TEXT NOT NULL," +
|
||||
"encryptedUsername TEXT NOT NULL," +
|
||||
"encryptedPassword TEXT NOT NULL," +
|
||||
"guid TEXT," +
|
||||
"encType INTEGER," +
|
||||
"timeCreated INTEGER," +
|
||||
"timeLastUsed INTEGER," +
|
||||
"timePasswordChanged INTEGER," +
|
||||
"timesUsed INTEGER)");
|
||||
yield aConnection.execute("CREATE TABLE moz_disabledHosts (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"hostname TEXT UNIQUE)");
|
||||
yield aConnection.execute("CREATE TABLE moz_deleted_logins (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"guid TEXT," +
|
||||
"timeDeleted INTEGER)");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new entry in the database resembling the given nsILoginInfo object.
|
||||
*/
|
||||
function promiseInsertLoginInfo(aConnection, aLoginInfo)
|
||||
{
|
||||
aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// We can't use the aLoginInfo object directly in the execute statement
|
||||
// because the bind code in Sqlite.jsm doesn't allow objects with extra
|
||||
// properties beyond those being binded. So we might as well use an array as
|
||||
// it is simpler.
|
||||
let values = [
|
||||
aLoginInfo.hostname,
|
||||
aLoginInfo.httpRealm,
|
||||
aLoginInfo.formSubmitURL,
|
||||
aLoginInfo.usernameField,
|
||||
aLoginInfo.passwordField,
|
||||
gLoginManagerCrypto.encrypt(aLoginInfo.username),
|
||||
gLoginManagerCrypto.encrypt(aLoginInfo.password),
|
||||
aLoginInfo.guid,
|
||||
aLoginInfo.encType,
|
||||
aLoginInfo.timeCreated,
|
||||
aLoginInfo.timeLastUsed,
|
||||
aLoginInfo.timePasswordChanged,
|
||||
aLoginInfo.timesUsed,
|
||||
];
|
||||
|
||||
return aConnection.execute("INSERT INTO moz_logins (hostname, " +
|
||||
"httpRealm, formSubmitURL, usernameField, " +
|
||||
"passwordField, encryptedUsername, " +
|
||||
"encryptedPassword, guid, encType, timeCreated, " +
|
||||
"timeLastUsed, timePasswordChanged, timesUsed) " +
|
||||
"VALUES (?" + ",?".repeat(12) + ")", values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new disabled host entry in the database.
|
||||
*/
|
||||
function promiseInsertDisabledHost(aConnection, aHostname)
|
||||
{
|
||||
return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " +
|
||||
"VALUES (?)", [aHostname]);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
/**
|
||||
* Imports login data from a SQLite file constructed using the test data.
|
||||
*/
|
||||
add_task(function test_import()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import.json").path);
|
||||
let loginsSqlite = getTempFile("test-logins.sqlite").path;
|
||||
|
||||
// Prepare the logins to be imported, including the nsILoginMetaInfo data.
|
||||
let loginList = TestData.loginList();
|
||||
for (let loginInfo of loginList) {
|
||||
loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
loginInfo.guid = gUUIDGenerator.generateUUID().toString();
|
||||
loginInfo.timeCreated = Date.now();
|
||||
loginInfo.timeLastUsed = Date.now();
|
||||
loginInfo.timePasswordChanged = Date.now();
|
||||
loginInfo.timesUsed = 1;
|
||||
}
|
||||
|
||||
// Create and populate the SQLite database first.
|
||||
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||
try {
|
||||
yield promiseCreateDatabaseSchema(connection);
|
||||
for (let loginInfo of loginList) {
|
||||
yield promiseInsertLoginInfo(connection, loginInfo);
|
||||
}
|
||||
yield promiseInsertDisabledHost(connection, "http://www.example.com");
|
||||
yield promiseInsertDisabledHost(connection, "https://www.example.org");
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// Verify that every login in the test data has a matching imported row.
|
||||
do_check_eq(loginList.length, store.data.logins.length);
|
||||
do_check_true(loginList.every(function (loginInfo) {
|
||||
return store.data.logins.some(function (loginDataItem) {
|
||||
let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername);
|
||||
let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword);
|
||||
return loginDataItem.hostname == loginInfo.hostname &&
|
||||
loginDataItem.httpRealm == loginInfo.httpRealm &&
|
||||
loginDataItem.formSubmitURL == loginInfo.formSubmitURL &&
|
||||
loginDataItem.usernameField == loginInfo.usernameField &&
|
||||
loginDataItem.passwordField == loginInfo.passwordField &&
|
||||
username == loginInfo.username &&
|
||||
password == loginInfo.password &&
|
||||
loginDataItem.guid == loginInfo.guid &&
|
||||
loginDataItem.encType == loginInfo.encType &&
|
||||
loginDataItem.timeCreated == loginInfo.timeCreated &&
|
||||
loginDataItem.timeLastUsed == loginInfo.timeLastUsed &&
|
||||
loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged &&
|
||||
loginDataItem.timesUsed == loginInfo.timesUsed;
|
||||
});
|
||||
}));
|
||||
|
||||
// Verify that disabled hosts have been imported.
|
||||
do_check_eq(store.data.disabledHosts.length, 2);
|
||||
do_check_true(store.data.disabledHosts.some(
|
||||
dataItem => dataItem.hostname == "http://www.example.com"));
|
||||
do_check_true(store.data.disabledHosts.some(
|
||||
dataItem => dataItem.hostname == "https://www.example.org"));
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests imports of NULL values due to a downgraded database.
|
||||
*/
|
||||
add_task(function test_import_downgraded()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-downgraded.json").path);
|
||||
let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path;
|
||||
let loginList = TestData.loginList();
|
||||
|
||||
// Create and populate the SQLite database first.
|
||||
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||
try {
|
||||
yield promiseCreateDatabaseSchema(connection);
|
||||
yield connection.setSchemaVersion(3);
|
||||
yield promiseInsertLoginInfo(connection, TestData.formLogin({
|
||||
guid: gUUIDGenerator.generateUUID().toString(),
|
||||
timeCreated: null,
|
||||
timeLastUsed: null,
|
||||
timePasswordChanged: null,
|
||||
timesUsed: 0,
|
||||
}));
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// Verify that the missing metadata was generated correctly.
|
||||
let loginItem = store.data.logins[0];
|
||||
let creationTime = loginItem.timeCreated;
|
||||
LoginTest.assertTimeIsAboutNow(creationTime);
|
||||
do_check_eq(loginItem.timeLastUsed, creationTime);
|
||||
do_check_eq(loginItem.timePasswordChanged, creationTime);
|
||||
do_check_eq(loginItem.timesUsed, 1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that importing from a SQLite file with database version 2 fails.
|
||||
*/
|
||||
add_task(function test_import_v2()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-v2.json").path);
|
||||
let loginsSqlite = do_get_file("data/signons-v2.sqlite").path;
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
try {
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
do_throw("The operation should have failed.");
|
||||
} catch (ex) { }
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a SQLite file, with database version 3.
|
||||
*/
|
||||
add_task(function test_import_v3()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-v3.json").path);
|
||||
let loginsSqlite = do_get_file("data/signons-v3.sqlite").path;
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// We only execute basic integrity checks.
|
||||
do_check_eq(store.data.logins[0].usernameField, "u1");
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
@ -0,0 +1,208 @@
|
||||
/* -*- Mode: C++; tab-width: 8; 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/ */
|
||||
|
||||
/**
|
||||
* Tests the LoginStore object.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
|
||||
const TEST_STORE_FILE_NAME = "test-logins.json";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
/**
|
||||
* Saves login data to a file, then reloads it.
|
||||
*/
|
||||
add_task(function test_save_reload()
|
||||
{
|
||||
let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
// The "load" method must be called before preparing the data to be saved.
|
||||
yield storeForSave.load();
|
||||
|
||||
let rawLoginData = {
|
||||
id: storeForSave.data.nextId++,
|
||||
hostname: "http://www.example.com",
|
||||
httpRealm: null,
|
||||
formSubmitURL: "http://www.example.com/submit-url",
|
||||
usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
|
||||
passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
|
||||
encryptedUsername: "(test)",
|
||||
encryptedPassword: "(test)",
|
||||
guid: "(test)",
|
||||
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||
timeCreated: Date.now(),
|
||||
timeLastUsed: Date.now(),
|
||||
timePasswordChanged: Date.now(),
|
||||
timesUsed: 1,
|
||||
};
|
||||
storeForSave.data.logins.push(rawLoginData);
|
||||
|
||||
storeForSave.data.disabledHosts.push("http://www.example.org");
|
||||
|
||||
yield storeForSave.save();
|
||||
|
||||
// Test the asynchronous initialization path.
|
||||
let storeForLoad = new LoginStore(storeForSave.path);
|
||||
yield storeForLoad.load();
|
||||
|
||||
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||
|
||||
// Test the synchronous initialization path.
|
||||
storeForLoad = new LoginStore(storeForSave.path);
|
||||
storeForLoad.ensureDataReady();
|
||||
|
||||
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that loading from a missing file results in empty arrays.
|
||||
*/
|
||||
add_task(function test_load_empty()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
do_check_false(yield OS.File.exists(store.path));
|
||||
|
||||
yield store.load();
|
||||
|
||||
do_check_false(yield OS.File.exists(store.path));
|
||||
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that saving empty data still overwrites any existing file.
|
||||
*/
|
||||
add_task(function test_save_empty()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
yield store.load();
|
||||
|
||||
let createdFile = yield OS.File.open(store.path, { create: true });
|
||||
yield createdFile.close();
|
||||
|
||||
yield store.save();
|
||||
|
||||
do_check_true(yield OS.File.exists(store.path));
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads data from a string in a predefined format. The purpose of this test is
|
||||
* to verify that the JSON format used in previous versions can be loaded.
|
||||
*/
|
||||
add_task(function test_load_string_predefined()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{" +
|
||||
"\"id\":1," +
|
||||
"\"hostname\":\"http://www.example.com\"," +
|
||||
"\"httpRealm\":null," +
|
||||
"\"formSubmitURL\":\"http://www.example.com/submit-url\"," +
|
||||
"\"usernameField\":\"usernameField\"," +
|
||||
"\"passwordField\":\"passwordField\"," +
|
||||
"\"encryptedUsername\":\"(test)\"," +
|
||||
"\"encryptedPassword\":\"(test)\"," +
|
||||
"\"guid\":\"(test)\"," +
|
||||
"\"encType\":1," +
|
||||
"\"timeCreated\":1262304000000," +
|
||||
"\"timeLastUsed\":1262390400000," +
|
||||
"\"timePasswordChanged\":1262476800000," +
|
||||
"\"timesUsed\":1}],\"disabledHosts\":[" +
|
||||
"\"http://www.example.org\"]}";
|
||||
|
||||
yield OS.File.writeAtomic(store.path,
|
||||
new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
yield store.load();
|
||||
|
||||
do_check_eq(store.data.logins.length, 1);
|
||||
do_check_matches(store.data.logins[0], {
|
||||
id: 1,
|
||||
hostname: "http://www.example.com",
|
||||
httpRealm: null,
|
||||
formSubmitURL: "http://www.example.com/submit-url",
|
||||
usernameField: "usernameField",
|
||||
passwordField: "passwordField",
|
||||
encryptedUsername: "(test)",
|
||||
encryptedPassword: "(test)",
|
||||
guid: "(test)",
|
||||
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||
timeCreated: 1262304000000,
|
||||
timeLastUsed: 1262390400000,
|
||||
timePasswordChanged: 1262476800000,
|
||||
timesUsed: 1,
|
||||
});
|
||||
|
||||
do_check_eq(store.data.disabledHosts.length, 1);
|
||||
do_check_eq(store.data.disabledHosts[0], "http://www.example.org");
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads login data from a malformed JSON string.
|
||||
*/
|
||||
add_task(function test_load_string_malformed()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||
"\"id\":1,";
|
||||
|
||||
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
yield store.load();
|
||||
|
||||
// A backup file should have been created.
|
||||
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||
yield OS.File.remove(store.path + ".corrupt");
|
||||
|
||||
// The store should be ready to accept new data.
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads login data from a malformed JSON string, using the synchronous
|
||||
* initialization path.
|
||||
*/
|
||||
add_task(function test_load_string_malformed_sync()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||
"\"id\":1,";
|
||||
|
||||
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
store.ensureDataReady();
|
||||
|
||||
// A backup file should have been created.
|
||||
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||
yield OS.File.remove(store.path + ".corrupt");
|
||||
|
||||
// The store should be ready to accept new data.
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
@ -3,6 +3,17 @@ head = head.js
|
||||
tail =
|
||||
support-files = data/**
|
||||
|
||||
# Test JSON file access and import from SQLite, not applicable to Android.
|
||||
[test_module_LoginImport.js]
|
||||
skip-if = os == "android"
|
||||
[test_module_LoginStore.js]
|
||||
skip-if = os == "android"
|
||||
|
||||
# Test SQLite database backup and migration, applicable to Android only.
|
||||
[test_storage_mozStorage.js]
|
||||
skip-if = os != "android"
|
||||
|
||||
# The following tests apply to any storage back-end.
|
||||
[test_disabled_hosts.js]
|
||||
[test_legacy_empty_formSubmitURL.js]
|
||||
[test_legacy_validation.js]
|
||||
@ -12,4 +23,3 @@ support-files = data/**
|
||||
[test_logins_search.js]
|
||||
[test_notifications.js]
|
||||
[test_storage.js]
|
||||
[test_storage_mozStorage.js]
|
||||
|
@ -142,7 +142,7 @@ nsUrlClassifierPrefixSet::GetPrefixes(uint32_t* aCount,
|
||||
NS_ENSURE_ARG_POINTER(aPrefixes);
|
||||
*aPrefixes = nullptr;
|
||||
|
||||
uint32_t itemCount = mIndexStarts.Length() + mDeltas.Length();
|
||||
uint64_t itemCount = mIndexStarts.Length() + mDeltas.Length();
|
||||
uint32_t* prefixArray = static_cast<uint32_t*>(nsMemory::Alloc(itemCount * sizeof(uint32_t)));
|
||||
NS_ENSURE_TRUE(prefixArray, NS_ERROR_OUT_OF_MEMORY);
|
||||
|
||||
@ -154,7 +154,7 @@ nsUrlClassifierPrefixSet::GetPrefixes(uint32_t* aCount,
|
||||
uint32_t start = mIndexStarts[i];
|
||||
uint32_t end = (i == (prefixIdxLength - 1)) ? mDeltas.Length()
|
||||
: mIndexStarts[i + 1];
|
||||
if (end > mDeltas.Length()) {
|
||||
if (end > mDeltas.Length() || (start > end)) {
|
||||
return NS_ERROR_FILE_CORRUPTED;
|
||||
}
|
||||
|
||||
@ -312,6 +312,9 @@ nsUrlClassifierPrefixSet::LoadFromFd(AutoFDClose& fileFd)
|
||||
NS_ENSURE_TRUE(read == toRead, NS_ERROR_FILE_CORRUPTED);
|
||||
read = PR_Read(fileFd, mIndexStarts.Elements(), toRead);
|
||||
NS_ENSURE_TRUE(read == toRead, NS_ERROR_FILE_CORRUPTED);
|
||||
if (indexSize != 0 && mIndexStarts[0] != 0) {
|
||||
return NS_ERROR_FILE_CORRUPTED;
|
||||
}
|
||||
if (deltaSize > 0) {
|
||||
toRead = deltaSize*sizeof(uint16_t);
|
||||
read = PR_Read(fileFd, mDeltas.Elements(), toRead);
|
||||
|
@ -1,8 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Components.utils.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
let tab;
|
||||
|
@ -16,12 +16,12 @@
|
||||
- Text on the button to go back to the main style editor -->
|
||||
<!ENTITY csscoverage.backButton "Back">
|
||||
|
||||
<!-- LOCALIZATION NOTE (csscoverage.unused, csscoverage.noMatch):
|
||||
<!-- LOCALIZATION NOTE (csscoverage.unused, csscoverage.noMatches):
|
||||
- This is the heading and body text for the CSS usage part of the report -->
|
||||
<!ENTITY csscoverage.unused "Unused Rules">
|
||||
<!ENTITY csscoverage.noMatch "No matches found for the following rules:">
|
||||
<!ENTITY csscoverage.noMatches "No matches found for the following rules:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (csscoverage.optimize):
|
||||
<!-- LOCALIZATION NOTE (csscoverage.optimize.header):
|
||||
- This is the heading for the CSS optimization part of the report -->
|
||||
<!ENTITY csscoverage.optimize.header "Optimizable Pages">
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<!ENTITY csscoverage.optimize.body2 "tags to the bottom of the page and creating a new inline">
|
||||
<!ENTITY csscoverage.optimize.body3 "element with the styles needed before the 'load' event to the top. Here are the style blocks you need:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (csscoverage.noPreload):
|
||||
<!-- LOCALIZATION NOTE (csscoverage.optimize.bodyX):
|
||||
- This is what we say when we have no optimization suggestions -->
|
||||
<!ENTITY csscoverage.optimize.bodyX "All rules are inlined.">
|
||||
|
||||
|
@ -64,6 +64,7 @@
|
||||
#include "GLContextCGL.h"
|
||||
#include "GLUploadHelpers.h"
|
||||
#include "ScopedGLHelpers.h"
|
||||
#include "HeapCopyOfStackArray.h"
|
||||
#include "mozilla/layers/GLManager.h"
|
||||
#include "mozilla/layers/CompositorOGL.h"
|
||||
#include "mozilla/layers/BasicCompositor.h"
|
||||
@ -2790,8 +2791,12 @@ GLPresenter::GLPresenter(GLContext* aContext)
|
||||
/* Then quad texcoords */
|
||||
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
|
||||
};
|
||||
mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER, sizeof(vertices), vertices, LOCAL_GL_STATIC_DRAW);
|
||||
mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
|
||||
HeapCopyOfStackArray<GLfloat> verticesOnHeap(vertices);
|
||||
mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER,
|
||||
verticesOnHeap.ByteLength(),
|
||||
verticesOnHeap.Data(),
|
||||
LOCAL_GL_STATIC_DRAW);
|
||||
mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
GLPresenter::~GLPresenter()
|
||||
@ -2810,18 +2815,24 @@ GLPresenter::BindAndDrawQuad(ShaderProgramOGL *aProgram,
|
||||
{
|
||||
mGLContext->MakeCurrent();
|
||||
|
||||
aProgram->SetLayerRect(aLayerRect);
|
||||
aProgram->SetTextureRect(aTextureRect);
|
||||
gfx::Rect layerRects[4];
|
||||
gfx::Rect textureRects[4];
|
||||
|
||||
layerRects[0] = aLayerRect;
|
||||
textureRects[0] = aTextureRect;
|
||||
|
||||
aProgram->SetLayerRects(layerRects);
|
||||
aProgram->SetTextureRects(textureRects);
|
||||
|
||||
GLuint vertAttribIndex = aProgram->AttribLocation(ShaderProgramOGL::VertexCoordAttrib);
|
||||
GLuint texCoordAttribIndex = aProgram->AttribLocation(ShaderProgramOGL::TexCoordAttrib);
|
||||
|
||||
mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO);
|
||||
mGLContext->fVertexAttribPointer(vertAttribIndex, 2,
|
||||
mGLContext->fVertexAttribPointer(vertAttribIndex, 4,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*)0);
|
||||
mGLContext->fEnableVertexAttribArray(vertAttribIndex);
|
||||
mGLContext->fVertexAttribPointer(texCoordAttribIndex, 2,
|
||||
mGLContext->fVertexAttribPointer(texCoordAttribIndex, 4,
|
||||
LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0,
|
||||
(GLvoid*) (sizeof(float)*4*2));
|
||||
mGLContext->fEnableVertexAttribArray(texCoordAttribIndex);
|
||||
|
@ -2481,18 +2481,12 @@ GetDPI(NSWindow* aWindow)
|
||||
[[[screen deviceDescription] objectForKey:@"NSScreenNumber"] intValue];
|
||||
CGFloat heightMM = ::CGDisplayScreenSize(displayID).height;
|
||||
size_t heightPx = ::CGDisplayPixelsHigh(displayID);
|
||||
CGFloat scaleFactor = [aWindow userSpaceScaleFactor];
|
||||
if (scaleFactor < 0.01 || heightMM < 1 || heightPx < 1) {
|
||||
if (heightMM < 1 || heightPx < 1) {
|
||||
// Something extremely bogus is going on
|
||||
return 96.0f;
|
||||
}
|
||||
|
||||
// Currently we don't do our own scaling to take account
|
||||
// of userSpaceScaleFactor, so every "pixel" we draw is actually
|
||||
// userSpaceScaleFactor screen pixels. So divide the screen height
|
||||
// by userSpaceScaleFactor to get the number of "device pixels"
|
||||
// available.
|
||||
float dpi = (heightPx / scaleFactor) / (heightMM / MM_PER_INCH_FLOAT);
|
||||
float dpi = heightPx / (heightMM / MM_PER_INCH_FLOAT);
|
||||
|
||||
// Account for HiDPI mode where Cocoa's "points" do not correspond to real
|
||||
// device pixels
|
||||
|
@ -129,6 +129,7 @@ protected:
|
||||
void GetScrollbarDrawInfo (HIThemeTrackDrawInfo& aTdi, nsIFrame *aFrame,
|
||||
const CGSize& aSize, bool aShouldGetButtonStates);
|
||||
nsIFrame* GetParentScrollbarFrame(nsIFrame *aFrame);
|
||||
bool IsParentScrollbarRolledOver(nsIFrame* aFrame);
|
||||
|
||||
private:
|
||||
NSButtonCell* mHelpButtonCell;
|
||||
|
@ -2068,6 +2068,21 @@ nsNativeThemeCocoa::DrawResizer(CGContextRef cgContext, const HIRect& aRect,
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
}
|
||||
|
||||
static bool
|
||||
ScrollbarTrackAndThumbDrawSeparately()
|
||||
{
|
||||
return nsLookAndFeel::UseOverlayScrollbars() || nsCocoaFeatures::OnLionOrLater();
|
||||
}
|
||||
|
||||
bool
|
||||
nsNativeThemeCocoa::IsParentScrollbarRolledOver(nsIFrame* aFrame)
|
||||
{
|
||||
nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame);
|
||||
return nsLookAndFeel::UseOverlayScrollbars()
|
||||
? CheckBooleanAttr(scrollbarFrame, nsGkAtoms::hover)
|
||||
: GetContentState(scrollbarFrame, NS_THEME_NONE).HasState(NS_EVENT_STATE_HOVER);
|
||||
}
|
||||
|
||||
static bool
|
||||
IsHiDPIContext(nsDeviceContext* aContext)
|
||||
{
|
||||
@ -2507,17 +2522,17 @@ nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext,
|
||||
|
||||
case NS_THEME_SCROLLBAR_SMALL:
|
||||
case NS_THEME_SCROLLBAR:
|
||||
if (!nsLookAndFeel::UseOverlayScrollbars()) {
|
||||
if (!ScrollbarTrackAndThumbDrawSeparately()) {
|
||||
DrawScrollbar(cgContext, macRect, aFrame);
|
||||
}
|
||||
break;
|
||||
case NS_THEME_SCROLLBAR_THUMB_VERTICAL:
|
||||
case NS_THEME_SCROLLBAR_THUMB_HORIZONTAL:
|
||||
if (nsLookAndFeel::UseOverlayScrollbars()) {
|
||||
if (ScrollbarTrackAndThumbDrawSeparately()) {
|
||||
BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars();
|
||||
BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBAR_THUMB_HORIZONTAL);
|
||||
BOOL isRolledOver = CheckBooleanAttr(GetParentScrollbarFrame(aFrame),
|
||||
nsGkAtoms::hover);
|
||||
if (!nsCocoaFeatures::OnMountainLionOrLater() || !isRolledOver) {
|
||||
BOOL isRolledOver = IsParentScrollbarRolledOver(aFrame);
|
||||
if (isOverlay && (!nsCocoaFeatures::OnMountainLionOrLater() || !isRolledOver)) {
|
||||
if (isHorizontal) {
|
||||
macRect.origin.y += 4;
|
||||
macRect.size.height -= 4;
|
||||
@ -2532,9 +2547,9 @@ nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext,
|
||||
const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame);
|
||||
CUIDraw([NSWindow coreUIRenderer], macRect, cgContext,
|
||||
(CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"kCUIWidgetOverlayScrollBar", @"widget",
|
||||
(isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget",
|
||||
@"regular", @"size",
|
||||
(isRolledOver ? @"rollover" : @""), @"state",
|
||||
(isRolledOver ? @"rollover" : @"normal"), @"state",
|
||||
(isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey",
|
||||
(isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey",
|
||||
[NSNumber numberWithBool:YES], @"indiconly",
|
||||
@ -2560,35 +2575,37 @@ nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext,
|
||||
break;
|
||||
case NS_THEME_SCROLLBAR_TRACK_HORIZONTAL:
|
||||
case NS_THEME_SCROLLBAR_TRACK_VERTICAL:
|
||||
if (nsLookAndFeel::UseOverlayScrollbars() &&
|
||||
CheckBooleanAttr(GetParentScrollbarFrame(aFrame), nsGkAtoms::hover)) {
|
||||
BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBAR_TRACK_HORIZONTAL);
|
||||
if (!nsCocoaFeatures::OnMountainLionOrLater()) {
|
||||
// On OSX 10.7, scrollbars don't grow when hovered. The adjustments
|
||||
// below were obtained by trial and error.
|
||||
if (isHorizontal) {
|
||||
macRect.origin.y += 2.0;
|
||||
} else {
|
||||
if (aFrame->StyleVisibility()->mDirection !=
|
||||
NS_STYLE_DIRECTION_RTL) {
|
||||
macRect.origin.x += 3.0;
|
||||
if (ScrollbarTrackAndThumbDrawSeparately()) {
|
||||
BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars();
|
||||
if (!isOverlay || IsParentScrollbarRolledOver(aFrame)) {
|
||||
BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBAR_TRACK_HORIZONTAL);
|
||||
if (isOverlay && !nsCocoaFeatures::OnMountainLionOrLater()) {
|
||||
// On OSX 10.7, scrollbars don't grow when hovered.
|
||||
// The adjustments below were obtained by trial and error.
|
||||
if (isHorizontal) {
|
||||
macRect.origin.y += 2.0;
|
||||
} else {
|
||||
macRect.origin.x -= 1.0;
|
||||
if (aFrame->StyleVisibility()->mDirection !=
|
||||
NS_STYLE_DIRECTION_RTL) {
|
||||
macRect.origin.x += 3.0;
|
||||
} else {
|
||||
macRect.origin.x -= 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame);
|
||||
CUIDraw([NSWindow coreUIRenderer], macRect, cgContext,
|
||||
(CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
(isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget",
|
||||
@"regular", @"size",
|
||||
(isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey",
|
||||
(isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey",
|
||||
[NSNumber numberWithBool:YES], @"noindicator",
|
||||
[NSNumber numberWithBool:YES], @"kCUIThumbProportionKey",
|
||||
[NSNumber numberWithBool:YES], @"is.flipped",
|
||||
nil],
|
||||
nil);
|
||||
}
|
||||
const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame);
|
||||
CUIDraw([NSWindow coreUIRenderer], macRect, cgContext,
|
||||
(CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"kCUIWidgetOverlayScrollBar", @"widget",
|
||||
@"regular", @"size",
|
||||
(isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey",
|
||||
(isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey",
|
||||
[NSNumber numberWithBool:YES], @"noindicator",
|
||||
[NSNumber numberWithBool:YES], @"kCUIThumbProportionKey",
|
||||
[NSNumber numberWithBool:YES], @"is.flipped",
|
||||
nil],
|
||||
nil);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -3232,6 +3249,18 @@ nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame, uint8_t aWidgetType,
|
||||
aAttribute == nsGkAtoms::open ||
|
||||
aAttribute == nsGkAtoms::hover)
|
||||
*aShouldRepaint = true;
|
||||
|
||||
if ((aWidgetType == NS_THEME_SCROLLBAR ||
|
||||
aWidgetType == NS_THEME_SCROLLBAR_SMALL) &&
|
||||
!ScrollbarTrackAndThumbDrawSeparately() &&
|
||||
(aAttribute == nsGkAtoms::curpos ||
|
||||
aAttribute == nsGkAtoms::minpos ||
|
||||
aAttribute == nsGkAtoms::maxpos ||
|
||||
aAttribute == nsGkAtoms::pageincrement)) {
|
||||
// 10.6-style scrollbars paint the thumb as part of the scrollbar,
|
||||
// so we need to invalidate the scrollbar when the thumb moves.
|
||||
*aShouldRepaint = true;
|
||||
}
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
|
@ -18,18 +18,27 @@ namespace widget {
|
||||
uint32_t
|
||||
ContentHelper::GetTouchActionFromFrame(nsIFrame* aFrame)
|
||||
{
|
||||
if (!aFrame || !aFrame->GetContent() || !aFrame->GetContent()->GetPrimaryFrame()) {
|
||||
// If frame is invalid or null then return default value.
|
||||
// If aFrame is null then return default value
|
||||
if (!aFrame) {
|
||||
return NS_STYLE_TOUCH_ACTION_AUTO;
|
||||
}
|
||||
|
||||
if (!aFrame->IsFrameOfType(nsIFrame::eSVG) && !aFrame->IsFrameOfType(nsIFrame::eBlockFrame)) {
|
||||
// Since touch-action property can be applied to only svg and block-level
|
||||
// elements we ignore frames of other types.
|
||||
// The touch-action CSS property applies to: all elements except:
|
||||
// non-replaced inline elements, table rows, row groups, table columns, and column groups
|
||||
bool isNonReplacedInlineElement = aFrame->IsFrameOfType(nsIFrame::eLineParticipant);
|
||||
if (isNonReplacedInlineElement) {
|
||||
return NS_STYLE_TOUCH_ACTION_AUTO;
|
||||
}
|
||||
|
||||
return (aFrame->GetContent()->GetPrimaryFrame()->StyleDisplay()->mTouchAction);
|
||||
const nsStyleDisplay* disp = aFrame->StyleDisplay();
|
||||
bool isTableElement = disp->IsInnerTableStyle() &&
|
||||
disp->mDisplay != NS_STYLE_DISPLAY_TABLE_CELL &&
|
||||
disp->mDisplay != NS_STYLE_DISPLAY_TABLE_CAPTION;
|
||||
if (isTableElement) {
|
||||
return NS_STYLE_TOUCH_ACTION_AUTO;
|
||||
}
|
||||
|
||||
return disp->mTouchAction;
|
||||
}
|
||||
|
||||
void
|
||||
|
Loading…
Reference in New Issue
Block a user