mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-18 15:55:36 +00:00
Merge m-c to m-i
MozReview-Commit-ID: 322JHuSlk86
This commit is contained in:
commit
3b8c995f23
@ -843,10 +843,6 @@ pref("media.webspeech.synth.enabled", true);
|
||||
// Enable Web Speech recognition API
|
||||
pref("media.webspeech.recognition.enable", true);
|
||||
|
||||
// Downloads API
|
||||
pref("dom.mozDownloads.enabled", true);
|
||||
pref("dom.downloads.max_retention_days", 7);
|
||||
|
||||
// External Helper Application Handling
|
||||
//
|
||||
// All external helper application handling can require the docshell to be
|
||||
|
@ -49,6 +49,7 @@ pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LO
|
||||
pref("extensions.webservice.discoverURL", "https://discovery.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
|
||||
pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/list/recommended/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox");
|
||||
pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
|
||||
pref("extensions.getAddons.themes.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes/?src=firefox");
|
||||
|
||||
pref("extensions.update.autoUpdateDefault", true);
|
||||
|
||||
|
@ -697,18 +697,6 @@ nsDefaultCommandLineHandler.prototype = {
|
||||
|
||||
/* nsICommandLineHandler */
|
||||
handle : function dch_handle(cmdLine) {
|
||||
// The -url flag is inserted by the operating system when the default
|
||||
// application handler is used. We check for default browser to remove
|
||||
// instances where users explicitly decide to "open with" the browser.
|
||||
// Note that users who launch firefox manually with the -url flag will
|
||||
// get erroneously counted.
|
||||
try {
|
||||
if (cmdLine.findFlag("url", false) &&
|
||||
ShellService.isDefaultBrowser(false, false)) {
|
||||
Services.telemetry.getHistogramById("FX_STARTUP_EXTERNAL_CONTENT_HANDLER").add();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
var urilist = [];
|
||||
|
||||
if (AppConstants.platform == "win") {
|
||||
|
@ -121,6 +121,16 @@ var gAdvancedPane = {
|
||||
return;
|
||||
var advancedPrefs = document.getElementById("advancedPrefs");
|
||||
var preference = document.getElementById("browser.preferences.advanced.selectedTabIndex");
|
||||
|
||||
// tabSelectionChanged gets called twice due to the selectedIndex being set
|
||||
// by both the selectedItem and selectedPanel callstacks. This guard is used
|
||||
// to prevent double-counting in Telemetry.
|
||||
if (preference.valueFromPreferences != advancedPrefs.selectedIndex) {
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_PREFERENCES_CATEGORY_OPENED")
|
||||
.add(telemetryBucketForCategory("advanced"));
|
||||
}
|
||||
|
||||
preference.valueFromPreferences = advancedPrefs.selectedIndex;
|
||||
},
|
||||
|
||||
|
@ -123,6 +123,36 @@ function init_dynamic_padding() {
|
||||
document.documentElement.appendChild(mediaStyle);
|
||||
}
|
||||
|
||||
function telemetryBucketForCategory(category) {
|
||||
switch (category) {
|
||||
case "general":
|
||||
case "search":
|
||||
case "content":
|
||||
case "applications":
|
||||
case "privacy":
|
||||
case "security":
|
||||
case "sync":
|
||||
return category;
|
||||
case "advanced":
|
||||
let advancedPaneTabs = document.getElementById("advancedPrefs");
|
||||
switch (advancedPaneTabs.selectedTab.id) {
|
||||
case "generalTab":
|
||||
return "advancedGeneral";
|
||||
case "dataChoicesTab":
|
||||
return "advancedDataChoices";
|
||||
case "networkTab":
|
||||
return "advancedNetwork";
|
||||
case "updateTab":
|
||||
return "advancedUpdates";
|
||||
case "encryptionTab":
|
||||
return "advancedCerts";
|
||||
}
|
||||
// fall-through for unknown.
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function onHashChange() {
|
||||
gotoPref();
|
||||
}
|
||||
@ -151,9 +181,9 @@ function gotoPref(aCategory) {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
let newHash = internalPrefCategoryNameToFriendlyName(category);
|
||||
let friendlyName = internalPrefCategoryNameToFriendlyName(category);
|
||||
if (gLastHash || category != kDefaultCategoryInternalName) {
|
||||
document.location.hash = newHash;
|
||||
document.location.hash = friendlyName;
|
||||
}
|
||||
// Need to set the gLastHash before setting categories.selectedItem since
|
||||
// the categories 'select' event will re-enter the gotoPref codepath.
|
||||
@ -163,6 +193,10 @@ function gotoPref(aCategory) {
|
||||
search(category, "data-category");
|
||||
let mainContent = document.querySelector(".main-content");
|
||||
mainContent.scrollTop = 0;
|
||||
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_PREFERENCES_CATEGORY_OPENED")
|
||||
.add(telemetryBucketForCategory(friendlyName));
|
||||
}
|
||||
|
||||
function search(aQuery, aAttribute) {
|
||||
|
@ -81,6 +81,7 @@ included_inclnames_to_ignore = set([
|
||||
'selfhosted.out.h', # generated in $OBJDIR
|
||||
'shellmoduleloader.out.h', # generated in $OBJDIR
|
||||
'unicode/timezone.h', # ICU
|
||||
'unicode/plurrule.h', # ICU
|
||||
'unicode/ucal.h', # ICU
|
||||
'unicode/uclean.h', # ICU
|
||||
'unicode/ucol.h', # ICU
|
||||
@ -90,6 +91,7 @@ included_inclnames_to_ignore = set([
|
||||
'unicode/unorm2.h', # ICU
|
||||
'unicode/unum.h', # ICU
|
||||
'unicode/unumsys.h', # ICU
|
||||
'unicode/upluralrules.h', # ICU
|
||||
'unicode/ustring.h', # ICU
|
||||
'unicode/utypes.h', # ICU
|
||||
'vtune/VTuneWrapper.h' # VTune
|
||||
|
12
dom/animation/test/crashtests/1323114-1.html
Normal file
12
dom/animation/test/crashtests/1323114-1.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id=a />
|
||||
<script>
|
||||
addEventListener("DOMContentLoaded", function(){
|
||||
a.animate([{"transform": "matrix3d(25,8788,-69,-24,-3,85,52,3,63,0,12,36810,-68,15,82,0) rotate(77rad)"}],
|
||||
{fill: "both", iterationStart: 45, iterationComposite: "accumulate"});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
18
dom/animation/test/crashtests/1323114-2.html
Normal file
18
dom/animation/test/crashtests/1323114-2.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
div {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: blue;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
<div id="div"></div>
|
||||
<script>
|
||||
addEventListener('DOMContentLoaded', function (){
|
||||
var target = document.getElementById('div');
|
||||
target.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
|
||||
{ transform: 'none' }],
|
||||
1000);
|
||||
});
|
||||
</script>
|
@ -13,3 +13,5 @@ pref(dom.animations-api.core.enabled,true) load 1277272-1.html
|
||||
pref(dom.animations-api.core.enabled,true) load 1290535-1.html
|
||||
pref(dom.animations-api.core.enabled,true) load 1304886-1.html
|
||||
pref(dom.animations-api.core.enabled,true) load 1322382-1.html
|
||||
pref(dom.animations-api.core.enabled,true) load 1323114-1.html
|
||||
pref(dom.animations-api.core.enabled,true) load 1323114-2.html
|
||||
|
252
dom/animation/test/css-animations/file_event-dispatch.html
Normal file
252
dom/animation/test/css-animations/file_event-dispatch.html
Normal file
@ -0,0 +1,252 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Tests for CSS animation event dispatch</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
|
||||
<script src="../testcommon.js"></script>
|
||||
<style>
|
||||
@keyframes anim {
|
||||
from { margin-left: 0px; }
|
||||
to { margin-left: 100px; }
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Helper class to record the elapsedTime member of each event.
|
||||
* The EventWatcher class in testharness.js allows us to wait on
|
||||
* multiple events in a certain order but only records the event
|
||||
* parameters of the most recent event.
|
||||
*/
|
||||
function AnimationEventHandler(target) {
|
||||
this.target = target;
|
||||
this.target.onanimationstart = function(evt) {
|
||||
this.animationstart = evt.elapsedTime;
|
||||
}.bind(this);
|
||||
this.target.onanimationiteration = function(evt) {
|
||||
this.animationiteration = evt.elapsedTime;
|
||||
}.bind(this);
|
||||
this.target.onanimationend = function(evt) {
|
||||
this.animationend = evt.elapsedTime;
|
||||
}.bind(this);
|
||||
}
|
||||
AnimationEventHandler.prototype.clear = function() {
|
||||
this.animationstart = undefined;
|
||||
this.animationiteration = undefined;
|
||||
this.animationend = undefined;
|
||||
}
|
||||
|
||||
function setupAnimation(t, animationStyle) {
|
||||
var div = addDiv(t, { style: "animation: " + animationStyle });
|
||||
var watcher = new EventWatcher(t, div, [ 'animationstart',
|
||||
'animationiteration',
|
||||
'animationend' ]);
|
||||
var handler = new AnimationEventHandler(div);
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
return [animation, watcher, handler, div];
|
||||
}
|
||||
|
||||
promise_test(function(t) {
|
||||
// Add 1ms delay to ensure that the delay is not included in the elapsedTime.
|
||||
const [animation, watcher] = setupAnimation(t, 'anim 100s 1ms');
|
||||
|
||||
return watcher.wait_for('animationstart').then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 0.0);
|
||||
});
|
||||
}, 'Idle -> Active');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] = setupAnimation(t, 'anim 100s');
|
||||
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart',
|
||||
'animationend' ]).then(function() {
|
||||
assert_equals(handler.animationstart, 0.0);
|
||||
assert_equals(handler.animationend, 100);
|
||||
});
|
||||
}, 'Idle -> After');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] =
|
||||
setupAnimation(t, 'anim 100s 100s paused');
|
||||
|
||||
return animation.ready.then(function() {
|
||||
// Seek to Active phase.
|
||||
animation.currentTime = 100 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationstart');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 0.0);
|
||||
});
|
||||
}, 'Before -> Active');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] =
|
||||
setupAnimation(t, 'anim 100s 100s paused');
|
||||
|
||||
return animation.ready.then(function() {
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart', 'animationend' ]);
|
||||
}).then(function(evt) {
|
||||
assert_equals(handler.animationstart, 0.0);
|
||||
assert_equals(handler.animationend, 100.0);
|
||||
});
|
||||
}, 'Before -> After');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] =
|
||||
setupAnimation(t, 'anim 100s 100s paused');
|
||||
|
||||
// Seek to Active phase.
|
||||
animation.currentTime = 100 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationstart').then(function() {
|
||||
// Seek to Before phase.
|
||||
animation.currentTime = 0;
|
||||
return watcher.wait_for('animationend');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 0.0);
|
||||
});
|
||||
}, 'Active -> Before');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] = setupAnimation(t, 'anim 100s paused');
|
||||
|
||||
return watcher.wait_for('animationstart').then(function(evt) {
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for('animationend');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 100.0);
|
||||
});
|
||||
}, 'Active -> After');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] =
|
||||
setupAnimation(t, 'anim 100s 100s paused');
|
||||
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart',
|
||||
'animationend' ]).then(function() {
|
||||
// Seek to Before phase.
|
||||
animation.currentTime = 0;
|
||||
handler.clear();
|
||||
return watcher.wait_for([ 'animationstart', 'animationend' ]);
|
||||
}).then(function() {
|
||||
assert_equals(handler.animationstart, 100.0);
|
||||
assert_equals(handler.animationend, 0.0);
|
||||
});
|
||||
}, 'After -> Before');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] =
|
||||
setupAnimation(t, 'anim 100s 100s paused');
|
||||
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart',
|
||||
'animationend' ]).then(function() {
|
||||
// Seek to Active phase.
|
||||
animation.currentTime = 100 * MS_PER_SEC;
|
||||
handler.clear();
|
||||
return watcher.wait_for('animationstart');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 100.0);
|
||||
});
|
||||
}, 'After -> Active');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler]
|
||||
= setupAnimation(t, 'anim 100s 100s 3 paused');
|
||||
|
||||
return animation.ready.then(function() {
|
||||
// Seek to iteration 0 (no animationiteration event should be dispatched)
|
||||
animation.currentTime = 100 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationstart');
|
||||
}).then(function(evt) {
|
||||
// Seek to iteration 2
|
||||
animation.currentTime = 300 * MS_PER_SEC;
|
||||
handler.clear();
|
||||
return watcher.wait_for('animationiteration');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 200);
|
||||
// Seek to After phase (no animationiteration event should be dispatched)
|
||||
animation.currentTime = 400 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationend');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 300);
|
||||
});
|
||||
}, 'Active -> Active (forwards)');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler] = setupAnimation(t, 'anim 100s 100s 3');
|
||||
|
||||
// Seek to After phase.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart',
|
||||
'animationend' ]).then(function() {
|
||||
// Seek to iteration 2 (no animationiteration event should be dispatched)
|
||||
animation.pause();
|
||||
animation.currentTime = 300 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationstart');
|
||||
}).then(function() {
|
||||
// Seek to mid of iteration 0 phase.
|
||||
animation.currentTime = 200 * MS_PER_SEC;
|
||||
return watcher.wait_for('animationiteration');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 200.0);
|
||||
// Seek to before phase (no animationiteration event should be dispatched)
|
||||
animation.currentTime = 0;
|
||||
return watcher.wait_for('animationend');
|
||||
});
|
||||
}, 'Active -> Active (backwards)');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler, div] =
|
||||
setupAnimation(t, 'anim 100s paused');
|
||||
return watcher.wait_for('animationstart').then(function(evt) {
|
||||
// Seek to Idle phase.
|
||||
div.style.display = 'none';
|
||||
flushComputedStyle(div);
|
||||
|
||||
// FIXME: bug 1302648: Add test for animationcancel event here.
|
||||
|
||||
// Restart this animation.
|
||||
div.style.display = '';
|
||||
return watcher.wait_for('animationstart');
|
||||
});
|
||||
}, 'Active -> Idle -> Active: animationstart is fired by restarting animation');
|
||||
|
||||
promise_test(function(t) {
|
||||
const [animation, watcher, handler, div] =
|
||||
setupAnimation(t, 'anim 100s 100s 2 paused');
|
||||
|
||||
// Make After.
|
||||
animation.finish();
|
||||
return watcher.wait_for([ 'animationstart',
|
||||
'animationend' ]).then(function(evt) {
|
||||
animation.playbackRate = -1;
|
||||
return watcher.wait_for('animationstart');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 200);
|
||||
// Seek to 1st iteration
|
||||
animation.currentTime = 200 * MS_PER_SEC - 1;
|
||||
return watcher.wait_for('animationiteration');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 100);
|
||||
// Seek to before
|
||||
animation.currentTime = 100 * MS_PER_SEC - 1;
|
||||
return watcher.wait_for('animationend');
|
||||
}).then(function(evt) {
|
||||
assert_equals(evt.elapsedTime, 0);
|
||||
assert_equals(animation.playState, 'running'); // delay
|
||||
});
|
||||
}, 'Negative playbackRate sanity test(Before -> Active -> Before)');
|
||||
|
||||
done();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
160
dom/animation/test/css-animations/file_event-order.html
Normal file
160
dom/animation/test/css-animations/file_event-order.html
Normal file
@ -0,0 +1,160 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Tests for CSS animation event order</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
|
||||
<script src="../testcommon.js"></script>
|
||||
<style>
|
||||
@keyframes anim {
|
||||
from { margin-left: 0px; }
|
||||
to { margin-left: 100px; }
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<script type='text/javascript'>
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Asserts that the set of actual and received events match.
|
||||
* @param actualEvents An array of the received AnimationEvent objects.
|
||||
* @param expectedEvents A series of array objects representing the expected
|
||||
* events, each having the form:
|
||||
* [ event type, target element, elapsed time ]
|
||||
*/
|
||||
function checkEvents(actualEvents, ...expectedEvents) {
|
||||
assert_equals(actualEvents.length, expectedEvents.length,
|
||||
`Number of actual events (${actualEvents.length}: \
|
||||
${actualEvents.map(event => event.type).join(', ')}) should match expected \
|
||||
events (${expectedEvents.map(event => event.type).join(', ')})`);
|
||||
|
||||
actualEvents.forEach((actualEvent, i) => {
|
||||
assert_equals(expectedEvents[i][0], actualEvent.type,
|
||||
'Event type should match');
|
||||
assert_equals(expectedEvents[i][1], actualEvent.target,
|
||||
'Event target should match');
|
||||
assert_equals(expectedEvents[i][2], actualEvent.elapsedTime,
|
||||
'Event\'s elapsed time should match');
|
||||
});
|
||||
}
|
||||
|
||||
function setupAnimation(t, animationStyle, receiveEvents) {
|
||||
const div = addDiv(t, { style: "animation: " + animationStyle });
|
||||
const watcher = new EventWatcher(t, div, [ 'animationstart',
|
||||
'animationiteration',
|
||||
'animationend' ]);
|
||||
|
||||
['start', 'iteration', 'end'].forEach(name => {
|
||||
div['onanimation' + name] = function(evt) {
|
||||
receiveEvents.push({ type: evt.type,
|
||||
target: evt.target,
|
||||
elapsedTime: evt.elapsedTime });
|
||||
}.bind(this);
|
||||
});
|
||||
|
||||
const animation = div.getAnimations()[0];
|
||||
|
||||
return [animation, watcher, div];
|
||||
}
|
||||
|
||||
promise_test(function(t) {
|
||||
let events = [];
|
||||
const [animation1, watcher1, div1] =
|
||||
setupAnimation(t, 'anim 100s 2 paused', events);
|
||||
const [animation2, watcher2, div2] =
|
||||
setupAnimation(t, 'anim 100s 2 paused', events);
|
||||
|
||||
return Promise.all([ watcher1.wait_for('animationstart'),
|
||||
watcher2.wait_for('animationstart') ]).then(function() {
|
||||
checkEvents(events, ['animationstart', div1, 0],
|
||||
['animationstart', div2, 0]);
|
||||
|
||||
events.length = 0; // Clear received event array
|
||||
|
||||
animation1.currentTime = 100 * MS_PER_SEC;
|
||||
animation2.currentTime = 100 * MS_PER_SEC;
|
||||
return Promise.all([ watcher1.wait_for('animationiteration'),
|
||||
watcher2.wait_for('animationiteration') ]);
|
||||
}).then(function() {
|
||||
checkEvents(events, ['animationiteration', div1, 100],
|
||||
['animationiteration', div2, 100]);
|
||||
|
||||
events.length = 0; // Clear received event array
|
||||
|
||||
animation1.finish();
|
||||
animation2.finish();
|
||||
|
||||
return Promise.all([ watcher1.wait_for('animationend'),
|
||||
watcher2.wait_for('animationend') ]);
|
||||
}).then(function() {
|
||||
checkEvents(events, ['animationend', div1, 200],
|
||||
['animationend', div2, 200]);
|
||||
});
|
||||
}, 'Test same events are ordered by elements.');
|
||||
|
||||
promise_test(function(t) {
|
||||
let events = [];
|
||||
const [animation1, watcher1, div1] =
|
||||
setupAnimation(t, 'anim 200s 400s', events);
|
||||
const [animation2, watcher2, div2] =
|
||||
setupAnimation(t, 'anim 300s 2', events);
|
||||
|
||||
return watcher2.wait_for('animationstart').then(function(evt) {
|
||||
animation1.currentTime = 400 * MS_PER_SEC;
|
||||
animation2.currentTime = 400 * MS_PER_SEC;
|
||||
|
||||
events.length = 0; // Clear received event array
|
||||
|
||||
return Promise.all([ watcher1.wait_for('animationstart'),
|
||||
watcher2.wait_for('animationiteration') ]);
|
||||
}).then(function() {
|
||||
checkEvents(events, ['animationiteration', div2, 300],
|
||||
['animationstart', div1, 0]);
|
||||
});
|
||||
}, 'Test start and iteration events are ordered by time.');
|
||||
|
||||
promise_test(function(t) {
|
||||
let events = [];
|
||||
const [animation1, watcher1, div1] =
|
||||
setupAnimation(t, 'anim 150s', events);
|
||||
const [animation2, watcher2, div2] =
|
||||
setupAnimation(t, 'anim 100s 2', events);
|
||||
|
||||
return Promise.all([ watcher1.wait_for('animationstart'),
|
||||
watcher2.wait_for('animationstart') ]).then(function() {
|
||||
animation1.currentTime = 150 * MS_PER_SEC;
|
||||
animation2.currentTime = 150 * MS_PER_SEC;
|
||||
|
||||
events.length = 0; // Clear received event array
|
||||
|
||||
return Promise.all([ watcher1.wait_for('animationend'),
|
||||
watcher2.wait_for('animationiteration') ]);
|
||||
}).then(function() {
|
||||
checkEvents(events, ['animationiteration', div2, 100],
|
||||
['animationend', div1, 150]);
|
||||
});
|
||||
}, 'Test iteration and end events are ordered by time.');
|
||||
|
||||
promise_test(function(t) {
|
||||
let events = [];
|
||||
const [animation1, watcher1, div1] =
|
||||
setupAnimation(t, 'anim 100s 100s', events);
|
||||
const [animation2, watcher2, div2] =
|
||||
setupAnimation(t, 'anim 100s 2', events);
|
||||
|
||||
animation1.finish();
|
||||
animation2.finish();
|
||||
|
||||
return Promise.all([ watcher1.wait_for([ 'animationstart',
|
||||
'animationend' ]),
|
||||
watcher2.wait_for([ 'animationstart',
|
||||
'animationend' ]) ]).then(function() {
|
||||
checkEvents(events, ['animationstart', div2, 0],
|
||||
['animationstart', div1, 0],
|
||||
['animationend', div1, 100],
|
||||
['animationend', div2, 200]);
|
||||
});
|
||||
}, 'Test start and end events are sorted correctly when fired simultaneously');
|
||||
|
||||
done();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
15
dom/animation/test/css-animations/test_event-dispatch.html
Normal file
15
dom/animation/test/css-animations/test_event-dispatch.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
'use strict';
|
||||
setup({explicit_done: true});
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{ "set": [["dom.animations-api.core.enabled", true]]},
|
||||
function() {
|
||||
window.open("file_event-dispatch.html");
|
||||
});
|
||||
</script>
|
||||
</html>
|
15
dom/animation/test/css-animations/test_event-order.html
Normal file
15
dom/animation/test/css-animations/test_event-order.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
'use strict';
|
||||
setup({explicit_done: true});
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{ "set": [["dom.animations-api.core.enabled", true]]},
|
||||
function() {
|
||||
window.open("file_event-order.html");
|
||||
});
|
||||
</script>
|
||||
</html>
|
@ -19,6 +19,8 @@ support-files =
|
||||
css-animations/file_document-get-animations.html
|
||||
css-animations/file_effect-target.html
|
||||
css-animations/file_element-get-animations.html
|
||||
css-animations/file_event-dispatch.html
|
||||
css-animations/file_event-order.html
|
||||
css-animations/file_keyframeeffect-getkeyframes.html
|
||||
css-animations/file_pseudoElement-get-animations.html
|
||||
css-transitions/file_animation-cancel.html
|
||||
@ -74,6 +76,8 @@ support-files =
|
||||
[css-animations/test_document-get-animations.html]
|
||||
[css-animations/test_effect-target.html]
|
||||
[css-animations/test_element-get-animations.html]
|
||||
[css-animations/test_event-dispatch.html]
|
||||
[css-animations/test_event-order.html]
|
||||
[css-animations/test_keyframeeffect-getkeyframes.html]
|
||||
[css-animations/test_pseudoElement-get-animations.html]
|
||||
[css-transitions/test_animation-cancel.html]
|
||||
|
@ -102,7 +102,6 @@
|
||||
#include "nsIDOMDocumentType.h"
|
||||
#include "nsGenericHTMLElement.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsContentCreatorFunctions.h"
|
||||
#include "nsIControllers.h"
|
||||
#include "nsView.h"
|
||||
|
@ -93,7 +93,6 @@
|
||||
#include "nsICategoryManager.h"
|
||||
#include "nsGenericHTMLElement.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsContentCreatorFunctions.h"
|
||||
#include "nsIControllers.h"
|
||||
#include "nsView.h"
|
||||
@ -264,12 +263,11 @@ nsIContent::GetDesiredIMEState()
|
||||
return IMEState(IMEState::DISABLED);
|
||||
}
|
||||
nsIEditor* editor = nsContentUtils::GetHTMLEditor(pc);
|
||||
nsCOMPtr<nsIEditorIMESupport> imeEditor = do_QueryInterface(editor);
|
||||
if (!imeEditor) {
|
||||
if (!editor) {
|
||||
return IMEState(IMEState::DISABLED);
|
||||
}
|
||||
IMEState state;
|
||||
imeEditor->GetPreferredIMEState(&state);
|
||||
editor->GetPreferredIMEState(&state);
|
||||
return state;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,6 @@
|
||||
#include "nsIDOMMutationEvent.h"
|
||||
#include "nsIDOMNodeList.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsILinkHandler.h"
|
||||
#include "mozilla/dom/NodeInfo.h"
|
||||
#include "mozilla/dom/NodeInfoInlines.h"
|
||||
|
@ -1,508 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
|
||||
Cu.import("resource://gre/modules/DownloadsIPC.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
|
||||
"@mozilla.org/childprocessmessagemanager;1",
|
||||
"nsIMessageSender");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
|
||||
"@mozilla.org/telephony/volume-service;1",
|
||||
"nsIVolumeService");
|
||||
|
||||
/**
|
||||
* The content process implementations of navigator.mozDownloadManager and its
|
||||
* DOMDownload download objects. Uses DownloadsIPC.jsm to communicate with
|
||||
* DownloadsAPI.jsm in the parent process.
|
||||
*/
|
||||
|
||||
function debug(aStr) {
|
||||
#ifdef MOZ_DEBUG
|
||||
dump("-*- DownloadsAPI.js : " + aStr + "\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
function DOMDownloadManagerImpl() {
|
||||
debug("DOMDownloadManagerImpl constructor");
|
||||
}
|
||||
|
||||
DOMDownloadManagerImpl.prototype = {
|
||||
__proto__: DOMRequestIpcHelper.prototype,
|
||||
|
||||
// nsIDOMGlobalPropertyInitializer implementation
|
||||
init: function(aWindow) {
|
||||
debug("DownloadsManager init");
|
||||
this.initDOMRequestHelper(aWindow,
|
||||
["Downloads:Added",
|
||||
"Downloads:Removed"]);
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
debug("uninit");
|
||||
downloadsCache.evict(this._window);
|
||||
},
|
||||
|
||||
set ondownloadstart(aHandler) {
|
||||
this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler);
|
||||
},
|
||||
|
||||
get ondownloadstart() {
|
||||
return this.__DOM_IMPL__.getEventHandler("ondownloadstart");
|
||||
},
|
||||
|
||||
getDownloads: function() {
|
||||
debug("getDownloads()");
|
||||
|
||||
return this.createPromise(function (aResolve, aReject) {
|
||||
DownloadsIPC.getDownloads().then(
|
||||
function(aDownloads) {
|
||||
// Turn the list of download objects into DOM objects and
|
||||
// send them.
|
||||
let array = new this._window.Array();
|
||||
for (let id in aDownloads) {
|
||||
let dom = createDOMDownloadObject(this._window, aDownloads[id]);
|
||||
array.push(this._prepareForContent(dom));
|
||||
}
|
||||
aResolve(array);
|
||||
}.bind(this),
|
||||
function() {
|
||||
aReject("GetDownloadsError");
|
||||
}
|
||||
);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
clearAllDone: function() {
|
||||
debug("clearAllDone()");
|
||||
// This is a void function; we just kick it off. No promises, etc.
|
||||
DownloadsIPC.clearAllDone();
|
||||
},
|
||||
|
||||
remove: function(aDownload) {
|
||||
debug("remove " + aDownload.url + " " + aDownload.id);
|
||||
return this.createPromise(function (aResolve, aReject) {
|
||||
if (!downloadsCache.has(this._window, aDownload.id)) {
|
||||
debug("no download " + aDownload.id);
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadsIPC.remove(aDownload.id).then(
|
||||
function(aResult) {
|
||||
let dom = createDOMDownloadObject(this._window, aResult);
|
||||
// Change the state right away to not race against the update message.
|
||||
dom.wrappedJSObject.state = "finalized";
|
||||
aResolve(this._prepareForContent(dom));
|
||||
}.bind(this),
|
||||
function() {
|
||||
aReject("RemoveError");
|
||||
}
|
||||
);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
adoptDownload: function(aAdoptDownloadDict) {
|
||||
// Our AdoptDownloadDict only includes simple types, which WebIDL enforces.
|
||||
// We have no object/any types so we do not need to worry about invoking
|
||||
// JSON.stringify (and it inheriting our security privileges).
|
||||
debug("adoptDownload");
|
||||
return this.createPromise(function (aResolve, aReject) {
|
||||
if (!aAdoptDownloadDict) {
|
||||
debug("Download dictionary is required!");
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
if (!aAdoptDownloadDict.storageName || !aAdoptDownloadDict.storagePath ||
|
||||
!aAdoptDownloadDict.contentType) {
|
||||
debug("Missing one of: storageName, storagePath, contentType");
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert storageName/storagePath to a local filesystem path.
|
||||
let volume;
|
||||
// getVolumeByName throws if you give it something it doesn't like
|
||||
// because XPConnect converts the NS_ERROR_NOT_AVAILABLE to an
|
||||
// exception. So catch it.
|
||||
try {
|
||||
volume = volumeService.getVolumeByName(aAdoptDownloadDict.storageName);
|
||||
} catch (ex) {}
|
||||
if (!volume) {
|
||||
debug("Invalid storage name: " + aAdoptDownloadDict.storageName);
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
let computedPath = volume.mountPoint + '/' +
|
||||
aAdoptDownloadDict.storagePath;
|
||||
// We validate that there is actually a file at the given path in the
|
||||
// parent process in DownloadsAPI.js because that's where the file
|
||||
// access would actually occur either way.
|
||||
|
||||
// Create a DownloadsAPI.jsm 'jsonDownload' style representation.
|
||||
let jsonDownload = {
|
||||
url: aAdoptDownloadDict.url,
|
||||
path: computedPath,
|
||||
contentType: aAdoptDownloadDict.contentType,
|
||||
startTime: aAdoptDownloadDict.startTime.valueOf() || Date.now(),
|
||||
sourceAppManifestURL: ""
|
||||
};
|
||||
|
||||
DownloadsIPC.adoptDownload(jsonDownload).then(
|
||||
function(aResult) {
|
||||
let domDownload = createDOMDownloadObject(this._window, aResult);
|
||||
aResolve(this._prepareForContent(domDownload));
|
||||
}.bind(this),
|
||||
function(aResult) {
|
||||
// This will be one of: AdoptError (generic catch-all),
|
||||
// AdoptNoSuchFile, AdoptFileIsDirectory
|
||||
aReject(aResult.error);
|
||||
}
|
||||
);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Turns a chrome download object into a content accessible one.
|
||||
* When we have __DOM_IMPL__ available we just use that, otherwise
|
||||
* we run _create() with the wrapped js object.
|
||||
*/
|
||||
_prepareForContent: function(aChromeObject) {
|
||||
if (aChromeObject.__DOM_IMPL__) {
|
||||
return aChromeObject.__DOM_IMPL__;
|
||||
}
|
||||
let res = this._window.DOMDownload._create(this._window,
|
||||
aChromeObject.wrappedJSObject);
|
||||
return res;
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
let data = aMessage.data;
|
||||
switch(aMessage.name) {
|
||||
case "Downloads:Added":
|
||||
debug("Adding " + uneval(data));
|
||||
let event = new this._window.DownloadEvent("downloadstart", {
|
||||
download:
|
||||
this._prepareForContent(createDOMDownloadObject(this._window, data))
|
||||
});
|
||||
this.__DOM_IMPL__.dispatchEvent(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIObserver,
|
||||
Ci.nsIDOMGlobalPropertyInitializer]),
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep track of download objects per window.
|
||||
*/
|
||||
var downloadsCache = {
|
||||
init: function() {
|
||||
this.cache = new WeakMap();
|
||||
},
|
||||
|
||||
has: function(aWindow, aId) {
|
||||
let downloads = this.cache.get(aWindow);
|
||||
return !!(downloads && downloads[aId]);
|
||||
},
|
||||
|
||||
get: function(aWindow, aDownload) {
|
||||
let downloads = this.cache.get(aWindow);
|
||||
if (!(downloads && downloads[aDownload.id])) {
|
||||
debug("Adding download " + aDownload.id + " to cache.");
|
||||
if (!downloads) {
|
||||
this.cache.set(aWindow, {});
|
||||
downloads = this.cache.get(aWindow);
|
||||
}
|
||||
// Create the object and add it to the cache.
|
||||
let impl = Cc["@mozilla.org/downloads/download;1"]
|
||||
.createInstance(Ci.nsISupports);
|
||||
impl.wrappedJSObject._init(aWindow, aDownload);
|
||||
downloads[aDownload.id] = impl;
|
||||
}
|
||||
return downloads[aDownload.id];
|
||||
},
|
||||
|
||||
evict: function(aWindow) {
|
||||
this.cache.delete(aWindow);
|
||||
}
|
||||
};
|
||||
|
||||
downloadsCache.init();
|
||||
|
||||
/**
|
||||
* The DOM facade of a download object.
|
||||
*/
|
||||
|
||||
function createDOMDownloadObject(aWindow, aDownload) {
|
||||
return downloadsCache.get(aWindow, aDownload);
|
||||
}
|
||||
|
||||
function DOMDownloadImpl() {
|
||||
debug("DOMDownloadImpl constructor ");
|
||||
|
||||
this.wrappedJSObject = this;
|
||||
this.totalBytes = 0;
|
||||
this.currentBytes = 0;
|
||||
this.url = null;
|
||||
this.path = null;
|
||||
this.storageName = null;
|
||||
this.storagePath = null;
|
||||
this.contentType = null;
|
||||
|
||||
/* fields that require getters/setters */
|
||||
this._error = null;
|
||||
this._startTime = new Date();
|
||||
this._state = "stopped";
|
||||
|
||||
/* private fields */
|
||||
this.id = null;
|
||||
}
|
||||
|
||||
DOMDownloadImpl.prototype = {
|
||||
|
||||
createPromise: function(aPromiseInit) {
|
||||
return new this._window.Promise(aPromiseInit);
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
debug("DOMDownloadImpl pause");
|
||||
let id = this.id;
|
||||
// We need to wrap the Promise.jsm promise in a "real" DOM promise...
|
||||
return this.createPromise(function(aResolve, aReject) {
|
||||
DownloadsIPC.pause(id).then(aResolve, aReject);
|
||||
});
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
debug("DOMDownloadImpl resume");
|
||||
let id = this.id;
|
||||
// We need to wrap the Promise.jsm promise in a "real" DOM promise...
|
||||
return this.createPromise(function(aResolve, aReject) {
|
||||
DownloadsIPC.resume(id).then(aResolve, aReject);
|
||||
});
|
||||
},
|
||||
|
||||
set onstatechange(aHandler) {
|
||||
this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler);
|
||||
},
|
||||
|
||||
get onstatechange() {
|
||||
return this.__DOM_IMPL__.getEventHandler("onstatechange");
|
||||
},
|
||||
|
||||
get error() {
|
||||
return this._error;
|
||||
},
|
||||
|
||||
set error(aError) {
|
||||
this._error = aError;
|
||||
},
|
||||
|
||||
get startTime() {
|
||||
return this._startTime;
|
||||
},
|
||||
|
||||
set startTime(aStartTime) {
|
||||
if (aStartTime instanceof Date) {
|
||||
this._startTime = aStartTime;
|
||||
}
|
||||
else {
|
||||
this._startTime = new Date(aStartTime);
|
||||
}
|
||||
},
|
||||
|
||||
get state() {
|
||||
return this._state;
|
||||
},
|
||||
|
||||
// We require a setter here to simplify the internals of the Download Manager
|
||||
// since we actually pass dummy JSON objects to the child process and update
|
||||
// them. This is the case for all other setters for read-only attributes
|
||||
// implemented in this object.
|
||||
set state(aState) {
|
||||
// We need to ensure that XPCOM consumers of this API respect the enum
|
||||
// values as well.
|
||||
if (["downloading",
|
||||
"stopped",
|
||||
"succeeded",
|
||||
"finalized"].indexOf(aState) != -1) {
|
||||
this._state = aState;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize a DOMDownload instance for the given window using the
|
||||
* 'jsonDownload' serialized format of the download encoded by
|
||||
* DownloadsAPI.jsm.
|
||||
*/
|
||||
_init: function(aWindow, aDownload) {
|
||||
this._window = aWindow;
|
||||
this.id = aDownload.id;
|
||||
this._update(aDownload);
|
||||
Services.obs.addObserver(this, "downloads-state-change-" + this.id,
|
||||
/* ownsWeak */ true);
|
||||
debug("observer set for " + this.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the state of the object and fires the statechange event.
|
||||
*/
|
||||
_update: function(aDownload) {
|
||||
debug("update " + uneval(aDownload));
|
||||
if (this.id != aDownload.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let props = ["totalBytes", "currentBytes", "url", "path", "storageName",
|
||||
"storagePath", "state", "contentType", "startTime",
|
||||
"sourceAppManifestURL"];
|
||||
let changed = false;
|
||||
let changedProps = {};
|
||||
|
||||
props.forEach((prop) => {
|
||||
if (prop in aDownload && (aDownload[prop] != this[prop])) {
|
||||
this[prop] = aDownload[prop];
|
||||
changedProps[prop] = changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// When the path changes, we should update the storage name and
|
||||
// storage path used for our downloaded file in case our download
|
||||
// was re-targetted to a different storage and/or filename.
|
||||
if (changedProps["path"]) {
|
||||
let storages = this._window.navigator.getDeviceStorages("sdcard");
|
||||
let preferredStorageName;
|
||||
// Use the first one or the default storage. Just like jsdownloads picks
|
||||
// the default / preferred download directory.
|
||||
storages.forEach((aStorage) => {
|
||||
if (aStorage.default || !preferredStorageName) {
|
||||
preferredStorageName = aStorage.storageName;
|
||||
}
|
||||
});
|
||||
// Now get the path for this storage area.
|
||||
let volume;
|
||||
if (preferredStorageName) {
|
||||
let volume = volumeService.getVolumeByName(preferredStorageName);
|
||||
if (volume) {
|
||||
// Finally, create the relative path of the file that can be used
|
||||
// later on to retrieve the file via DeviceStorage. Our path
|
||||
// needs to omit the starting '/'.
|
||||
this.storageName = preferredStorageName;
|
||||
this.storagePath =
|
||||
this.path.substring(this.path.indexOf(volume.mountPoint) +
|
||||
volume.mountPoint.length + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aDownload.error) {
|
||||
//
|
||||
// When we get a generic error failure back from the js downloads api
|
||||
// we will verify the status of device storage to see if we can't provide
|
||||
// a better error result value.
|
||||
//
|
||||
// XXX If these checks expand further, consider moving them into their
|
||||
// own function.
|
||||
//
|
||||
let result = aDownload.error.result;
|
||||
let storage = this._window.navigator.getDeviceStorage("sdcard");
|
||||
|
||||
// If we don't have access to device storage we'll opt out of these
|
||||
// extra checks as they are all dependent on the state of the storage.
|
||||
if (result == Cr.NS_ERROR_FAILURE && storage) {
|
||||
// We will delay sending the notification until we've inferred which
|
||||
// error is really happening.
|
||||
changed = false;
|
||||
debug("Attempting to infer error via device storage sanity checks.");
|
||||
// Get device storage and request availability status.
|
||||
let available = storage.available();
|
||||
available.onsuccess = (function() {
|
||||
debug("Storage Status = '" + available.result + "'");
|
||||
let inferredError = result;
|
||||
switch (available.result) {
|
||||
case "unavailable":
|
||||
inferredError = Cr.NS_ERROR_FILE_NOT_FOUND;
|
||||
break;
|
||||
case "shared":
|
||||
inferredError = Cr.NS_ERROR_FILE_ACCESS_DENIED;
|
||||
break;
|
||||
}
|
||||
this._updateWithError(aDownload, inferredError);
|
||||
}).bind(this);
|
||||
available.onerror = (function() {
|
||||
this._updateWithError(aDownload, result);
|
||||
}).bind(this);
|
||||
}
|
||||
|
||||
this.error =
|
||||
new this._window.DOMError("DownloadError", result);
|
||||
} else {
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
// The visible state has not changed, so no need to fire an event.
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendStateChange();
|
||||
},
|
||||
|
||||
_updateWithError: function(aDownload, aError) {
|
||||
this.error =
|
||||
new this._window.DOMError("DownloadError", aError);
|
||||
this._sendStateChange();
|
||||
},
|
||||
|
||||
_sendStateChange: function() {
|
||||
// __DOM_IMPL__ may not be available at first update.
|
||||
if (this.__DOM_IMPL__) {
|
||||
let event = new this._window.DownloadEvent("statechange", {
|
||||
download: this.__DOM_IMPL__
|
||||
});
|
||||
debug("Dispatching statechange event. state=" + this.state);
|
||||
this.__DOM_IMPL__.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
debug("DOMDownloadImpl observe " + aTopic);
|
||||
if (aTopic !== "downloads-state-change-" + this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let download = JSON.parse(aData);
|
||||
// We get the start time as milliseconds, not as a Date object.
|
||||
if (download.startTime) {
|
||||
download.startTime = new Date(download.startTime);
|
||||
}
|
||||
this._update(download);
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl,
|
||||
DOMDownloadImpl]);
|
@ -1,360 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [];
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Downloads.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
|
||||
"@mozilla.org/parentprocessmessagemanager;1",
|
||||
"nsIMessageBroadcaster");
|
||||
|
||||
/**
|
||||
* Parent process logic that services download API requests from the
|
||||
* DownloadAPI.js instances in content processeses. The actual work of managing
|
||||
* downloads is done by Toolkit's Downloads.jsm. This module is loaded by B2G's
|
||||
* shell.js
|
||||
*/
|
||||
|
||||
function debug(aStr) {
|
||||
#ifdef MOZ_DEBUG
|
||||
dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
function sendPromiseMessage(aMm, aMessageName, aData, aError) {
|
||||
debug("sendPromiseMessage " + aMessageName);
|
||||
let msg = {
|
||||
id: aData.id,
|
||||
promiseId: aData.promiseId
|
||||
};
|
||||
|
||||
if (aError) {
|
||||
msg.error = aError;
|
||||
}
|
||||
|
||||
aMm.sendAsyncMessage(aMessageName, msg);
|
||||
}
|
||||
|
||||
var DownloadsAPI = {
|
||||
init: function() {
|
||||
debug("init");
|
||||
|
||||
this._ids = new WeakMap(); // Maps toolkit download objects to ids.
|
||||
this._index = {}; // Maps ids to downloads.
|
||||
|
||||
["Downloads:GetList",
|
||||
"Downloads:ClearAllDone",
|
||||
"Downloads:Remove",
|
||||
"Downloads:Pause",
|
||||
"Downloads:Resume",
|
||||
"Downloads:Adopt"].forEach((msgName) => {
|
||||
ppmm.addMessageListener(msgName, this);
|
||||
});
|
||||
|
||||
let self = this;
|
||||
Task.spawn(function () {
|
||||
let list = yield Downloads.getList(Downloads.ALL);
|
||||
yield list.addView(self);
|
||||
|
||||
debug("view added to download list.");
|
||||
}).then(null, Components.utils.reportError);
|
||||
|
||||
this._currentId = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a unique id for each download, hashing the url and the path.
|
||||
*/
|
||||
downloadId: function(aDownload) {
|
||||
let id = this._ids.get(aDownload, null);
|
||||
if (!id) {
|
||||
id = "download-" + this._currentId++;
|
||||
this._ids.set(aDownload, id);
|
||||
this._index[id] = aDownload;
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
getDownloadById: function(aId) {
|
||||
return this._index[aId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a download object into a plain json object that we'll
|
||||
* send to the DOM side.
|
||||
*/
|
||||
jsonDownload: function(aDownload) {
|
||||
let res = {
|
||||
totalBytes: aDownload.totalBytes,
|
||||
currentBytes: aDownload.currentBytes,
|
||||
url: aDownload.source.url,
|
||||
path: aDownload.target.path,
|
||||
contentType: aDownload.contentType,
|
||||
startTime: aDownload.startTime.getTime(),
|
||||
sourceAppManifestURL: aDownload._unknownProperties &&
|
||||
aDownload._unknownProperties.sourceAppManifestURL
|
||||
};
|
||||
|
||||
if (aDownload.error) {
|
||||
res.error = aDownload.error;
|
||||
}
|
||||
|
||||
res.id = this.downloadId(aDownload);
|
||||
|
||||
// The state of the download. Can be any of "downloading", "stopped",
|
||||
// "succeeded", finalized".
|
||||
|
||||
// Default to "stopped"
|
||||
res.state = "stopped";
|
||||
if (!aDownload.stopped &&
|
||||
!aDownload.canceled &&
|
||||
!aDownload.succeeded &&
|
||||
!aDownload.DownloadError) {
|
||||
res.state = "downloading";
|
||||
} else if (aDownload.succeeded) {
|
||||
res.state = "succeeded";
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* download view methods.
|
||||
*/
|
||||
onDownloadAdded: function(aDownload) {
|
||||
let download = this.jsonDownload(aDownload);
|
||||
debug("onDownloadAdded " + uneval(download));
|
||||
ppmm.broadcastAsyncMessage("Downloads:Added", download);
|
||||
},
|
||||
|
||||
onDownloadRemoved: function(aDownload) {
|
||||
let download = this.jsonDownload(aDownload);
|
||||
download.state = "finalized";
|
||||
debug("onDownloadRemoved " + uneval(download));
|
||||
ppmm.broadcastAsyncMessage("Downloads:Removed", download);
|
||||
this._index[this._ids.get(aDownload)] = null;
|
||||
this._ids.delete(aDownload);
|
||||
},
|
||||
|
||||
onDownloadChanged: function(aDownload) {
|
||||
let download = this.jsonDownload(aDownload);
|
||||
debug("onDownloadChanged " + uneval(download));
|
||||
ppmm.broadcastAsyncMessage("Downloads:Changed", download);
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
debug("message: " + aMessage.name);
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "Downloads:GetList":
|
||||
this.getList(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:ClearAllDone":
|
||||
this.clearAllDone(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:Remove":
|
||||
this.remove(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:Pause":
|
||||
this.pause(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:Resume":
|
||||
this.resume(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:Adopt":
|
||||
this.adoptDownload(aMessage.data, aMessage.target);
|
||||
break;
|
||||
default:
|
||||
debug("Invalid message: " + aMessage.name);
|
||||
}
|
||||
},
|
||||
|
||||
getList: function(aData, aMm) {
|
||||
debug("getList called!");
|
||||
let self = this;
|
||||
Task.spawn(function () {
|
||||
let list = yield Downloads.getList(Downloads.ALL);
|
||||
let downloads = yield list.getAll();
|
||||
let res = [];
|
||||
downloads.forEach((aDownload) => {
|
||||
res.push(self.jsonDownload(aDownload));
|
||||
});
|
||||
aMm.sendAsyncMessage("Downloads:GetList:Return", res);
|
||||
}).then(null, Components.utils.reportError);
|
||||
},
|
||||
|
||||
clearAllDone: function(aData, aMm) {
|
||||
debug("clearAllDone called!");
|
||||
Task.spawn(function () {
|
||||
let list = yield Downloads.getList(Downloads.ALL);
|
||||
list.removeFinished();
|
||||
}).then(null, Components.utils.reportError);
|
||||
},
|
||||
|
||||
remove: function(aData, aMm) {
|
||||
debug("remove id " + aData.id);
|
||||
let download = this.getDownloadById(aData.id);
|
||||
if (!download) {
|
||||
sendPromiseMessage(aMm, "Downloads:Remove:Return",
|
||||
aData, "NoSuchDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
Task.spawn(function() {
|
||||
yield download.finalize(true);
|
||||
let list = yield Downloads.getList(Downloads.ALL);
|
||||
yield list.remove(download);
|
||||
}).then(
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Remove:Return", aData);
|
||||
},
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Remove:Return",
|
||||
aData, "RemoveError");
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
pause: function(aData, aMm) {
|
||||
debug("pause id " + aData.id);
|
||||
let download = this.getDownloadById(aData.id);
|
||||
if (!download) {
|
||||
sendPromiseMessage(aMm, "Downloads:Pause:Return",
|
||||
aData, "NoSuchDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
download.cancel().then(
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Pause:Return", aData);
|
||||
},
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Pause:Return",
|
||||
aData, "PauseError");
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
resume: function(aData, aMm) {
|
||||
debug("resume id " + aData.id);
|
||||
let download = this.getDownloadById(aData.id);
|
||||
if (!download) {
|
||||
sendPromiseMessage(aMm, "Downloads:Resume:Return",
|
||||
aData, "NoSuchDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
download.start().then(
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Resume:Return", aData);
|
||||
},
|
||||
function() {
|
||||
sendPromiseMessage(aMm, "Downloads:Resume:Return",
|
||||
aData, "ResumeError");
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Receive a download to adopt in the same representation we produce from
|
||||
* our "jsonDownload" normalizer and add it to the list of downloads.
|
||||
*/
|
||||
adoptDownload: function(aData, aMm) {
|
||||
let adoptJsonRep = aData.jsonDownload;
|
||||
debug("adoptDownload " + uneval(adoptJsonRep));
|
||||
|
||||
Task.spawn(function* () {
|
||||
// Verify that the file exists on disk. This will result in a rejection
|
||||
// if the file does not exist. We will also use this information for the
|
||||
// file size to avoid weird inconsistencies. We ignore the filesystem
|
||||
// timestamp in favor of whatever the caller is telling us.
|
||||
let fileInfo = yield OS.File.stat(adoptJsonRep.path);
|
||||
|
||||
// We also require that the file is not a directory.
|
||||
if (fileInfo.isDir) {
|
||||
throw new Error("AdoptFileIsDirectory");
|
||||
}
|
||||
|
||||
// We need to create a Download instance to add to the list. Create a
|
||||
// serialized representation and then from there the instance.
|
||||
let serializedRep = {
|
||||
// explicit initializations in toSerializable
|
||||
source: {
|
||||
url: adoptJsonRep.url
|
||||
// This is where isPrivate would go if adoption supported private
|
||||
// browsing.
|
||||
},
|
||||
target: {
|
||||
path: adoptJsonRep.path,
|
||||
},
|
||||
startTime: adoptJsonRep.startTime,
|
||||
// kPlainSerializableDownloadProperties propagations
|
||||
succeeded: true, // (all adopted downloads are required to be completed)
|
||||
totalBytes: fileInfo.size,
|
||||
contentType: adoptJsonRep.contentType,
|
||||
// unknown properties added/used by the DownloadsAPI
|
||||
currentBytes: fileInfo.size,
|
||||
sourceAppManifestURL: adoptJsonRep.sourceAppManifestURL
|
||||
};
|
||||
|
||||
let download = yield Downloads.createDownload(serializedRep);
|
||||
|
||||
// The ALL list is a DownloadCombinedList instance that combines the
|
||||
// PUBLIC (persisted to disk) and PRIVATE (ephemeral) download lists..
|
||||
// When we call add on it, it dispatches to the appropriate list based on
|
||||
// the 'isPrivate' field of the source. (Which we don't initialize and
|
||||
// defaults to false.)
|
||||
let allDownloadList = yield Downloads.getList(Downloads.ALL);
|
||||
|
||||
// This add will automatically notify all views of the added download,
|
||||
// including DownloadsAPI instances and the DownloadAutoSaveView that's
|
||||
// subscribed to the PUBLIC list and will save the download.
|
||||
yield allDownloadList.add(download);
|
||||
|
||||
debug("download adopted");
|
||||
// The notification above occurred synchronously, and so we will have
|
||||
// already dispatched an added notification for our download to the child
|
||||
// process in question. As such, we only need to relay the download id
|
||||
// since the download will already have been cached.
|
||||
return download;
|
||||
}.bind(this)).then(
|
||||
(download) => {
|
||||
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
|
||||
{
|
||||
id: this.downloadId(download),
|
||||
promiseId: aData.promiseId
|
||||
});
|
||||
},
|
||||
(ex) => {
|
||||
let reportAs = "AdoptError";
|
||||
// Provide better error codes for expected errors.
|
||||
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
reportAs = "AdoptNoSuchFile";
|
||||
} else if (ex.message === "AdoptFileIsDirectory") {
|
||||
reportAs = ex.message;
|
||||
} else {
|
||||
// Anything else is unexpected and should be reported to help track
|
||||
// down what's going wrong.
|
||||
debug("unexpected download error: " + ex);
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
|
||||
{
|
||||
promiseId: aData.promiseId
|
||||
},
|
||||
reportAs);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
DownloadsAPI.init();
|
@ -1,6 +0,0 @@
|
||||
# DownloadsAPI.js
|
||||
component {c6587afa-0696-469f-9eff-9dac0dd727fe} DownloadsAPI.js
|
||||
contract @mozilla.org/downloads/manager;1 {c6587afa-0696-469f-9eff-9dac0dd727fe}
|
||||
|
||||
component {96b81b99-aa96-439d-8c59-92eeed34705f} DownloadsAPI.js
|
||||
contract @mozilla.org/downloads/download;1 {96b81b99-aa96-439d-8c59-92eeed34705f}
|
@ -1,224 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["DownloadsIPC"];
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
|
||||
"@mozilla.org/childprocessmessagemanager;1",
|
||||
"nsIMessageSender");
|
||||
|
||||
/**
|
||||
* This module lives in the child process and receives the ipc messages
|
||||
* from the parent. It saves the download's state and redispatch changes
|
||||
* to DOM objects using an observer notification.
|
||||
*
|
||||
* This module needs to be loaded once and only once per process.
|
||||
*/
|
||||
|
||||
function debug(aStr) {
|
||||
#ifdef MOZ_DEBUG
|
||||
dump("-*- DownloadsIPC.jsm : " + aStr + "\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
const ipcMessages = ["Downloads:Added",
|
||||
"Downloads:Removed",
|
||||
"Downloads:Changed",
|
||||
"Downloads:GetList:Return",
|
||||
"Downloads:Remove:Return",
|
||||
"Downloads:Pause:Return",
|
||||
"Downloads:Resume:Return",
|
||||
"Downloads:Adopt:Return"];
|
||||
|
||||
this.DownloadsIPC = {
|
||||
downloads: {},
|
||||
|
||||
init: function() {
|
||||
debug("init");
|
||||
Services.obs.addObserver(this, "xpcom-shutdown", false);
|
||||
ipcMessages.forEach((aMessage) => {
|
||||
cpmm.addMessageListener(aMessage, this);
|
||||
});
|
||||
|
||||
// We need to get the list of current downloads.
|
||||
this.ready = false;
|
||||
this.getListPromises = [];
|
||||
this.downloadPromises = {};
|
||||
cpmm.sendAsyncMessage("Downloads:GetList", {});
|
||||
this._promiseId = 0;
|
||||
},
|
||||
|
||||
notifyChanges: function(aId) {
|
||||
// TODO: use the subject instead of stringifying.
|
||||
if (this.downloads[aId]) {
|
||||
debug("notifyChanges notifying changes for " + aId);
|
||||
Services.obs.notifyObservers(null, "downloads-state-change-" + aId,
|
||||
JSON.stringify(this.downloads[aId]));
|
||||
} else {
|
||||
debug("notifyChanges failed for " + aId)
|
||||
}
|
||||
},
|
||||
|
||||
_updateDownloadsArray: function(aDownloads) {
|
||||
this.downloads = [];
|
||||
// We actually have an array of downloads.
|
||||
aDownloads.forEach((aDownload) => {
|
||||
this.downloads[aDownload.id] = aDownload;
|
||||
});
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
let download = aMessage.data;
|
||||
debug("message: " + aMessage.name);
|
||||
switch(aMessage.name) {
|
||||
case "Downloads:GetList:Return":
|
||||
this._updateDownloadsArray(download);
|
||||
|
||||
if (!this.ready) {
|
||||
this.getListPromises.forEach(aPromise =>
|
||||
aPromise.resolve(this.downloads));
|
||||
this.getListPromises.length = 0;
|
||||
}
|
||||
this.ready = true;
|
||||
break;
|
||||
case "Downloads:Added":
|
||||
this.downloads[download.id] = download;
|
||||
this.notifyChanges(download.id);
|
||||
break;
|
||||
case "Downloads:Removed":
|
||||
if (this.downloads[download.id]) {
|
||||
this.downloads[download.id] = download;
|
||||
this.notifyChanges(download.id);
|
||||
delete this.downloads[download.id];
|
||||
}
|
||||
break;
|
||||
case "Downloads:Changed":
|
||||
// Only update properties that actually changed.
|
||||
let cached = this.downloads[download.id];
|
||||
if (!cached) {
|
||||
debug("No download found for " + download.id);
|
||||
return;
|
||||
}
|
||||
let props = ["totalBytes", "currentBytes", "url", "path", "state",
|
||||
"contentType", "startTime"];
|
||||
let changed = false;
|
||||
|
||||
props.forEach((aProp) => {
|
||||
if (download[aProp] && (download[aProp] != cached[aProp])) {
|
||||
cached[aProp] = download[aProp];
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Updating the error property. We always get a 'state' change as
|
||||
// well.
|
||||
cached.error = download.error;
|
||||
|
||||
if (changed) {
|
||||
this.notifyChanges(download.id);
|
||||
}
|
||||
break;
|
||||
case "Downloads:Remove:Return":
|
||||
case "Downloads:Pause:Return":
|
||||
case "Downloads:Resume:Return":
|
||||
case "Downloads:Adopt:Return":
|
||||
if (this.downloadPromises[download.promiseId]) {
|
||||
if (!download.error) {
|
||||
this.downloadPromises[download.promiseId].resolve(download);
|
||||
} else {
|
||||
this.downloadPromises[download.promiseId].reject(download);
|
||||
}
|
||||
delete this.downloadPromises[download.promiseId];
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a promise that is resolved with the list of current downloads.
|
||||
*/
|
||||
getDownloads: function() {
|
||||
debug("getDownloads()");
|
||||
let deferred = Promise.defer();
|
||||
if (this.ready) {
|
||||
debug("Returning existing list.");
|
||||
deferred.resolve(this.downloads);
|
||||
} else {
|
||||
this.getListPromises.push(deferred);
|
||||
}
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Void function to trigger removal of completed downloads.
|
||||
*/
|
||||
clearAllDone: function() {
|
||||
debug("clearAllDone");
|
||||
cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
|
||||
},
|
||||
|
||||
promiseId: function() {
|
||||
return this._promiseId++;
|
||||
},
|
||||
|
||||
remove: function(aId) {
|
||||
debug("remove " + aId);
|
||||
let deferred = Promise.defer();
|
||||
let pId = this.promiseId();
|
||||
this.downloadPromises[pId] = deferred;
|
||||
cpmm.sendAsyncMessage("Downloads:Remove",
|
||||
{ id: aId, promiseId: pId });
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
pause: function(aId) {
|
||||
debug("pause " + aId);
|
||||
let deferred = Promise.defer();
|
||||
let pId = this.promiseId();
|
||||
this.downloadPromises[pId] = deferred;
|
||||
cpmm.sendAsyncMessage("Downloads:Pause",
|
||||
{ id: aId, promiseId: pId });
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
resume: function(aId) {
|
||||
debug("resume " + aId);
|
||||
let deferred = Promise.defer();
|
||||
let pId = this.promiseId();
|
||||
this.downloadPromises[pId] = deferred;
|
||||
cpmm.sendAsyncMessage("Downloads:Resume",
|
||||
{ id: aId, promiseId: pId });
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
adoptDownload: function(aJsonDownload) {
|
||||
debug("adoptDownload");
|
||||
let deferred = Promise.defer();
|
||||
let pId = this.promiseId();
|
||||
this.downloadPromises[pId] = deferred;
|
||||
cpmm.sendAsyncMessage("Downloads:Adopt",
|
||||
{ jsonDownload: aJsonDownload, promiseId: pId });
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
if (aTopic == "xpcom-shutdown") {
|
||||
ipcMessages.forEach((aMessage) => {
|
||||
cpmm.removeMessageListener(aMessage, this);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DownloadsIPC.init();
|
@ -1,21 +0,0 @@
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
if CONFIG["MOZ_B2G"]:
|
||||
MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'DownloadsAPI.manifest',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'DownloadsAPI.js',
|
||||
]
|
||||
|
||||
EXTRA_PP_JS_MODULES += [
|
||||
'DownloadsAPI.jsm',
|
||||
'DownloadsIPC.jsm',
|
||||
]
|
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* A helper to clear out the existing downloads known to the mozDownloadManager
|
||||
* / downloads.js.
|
||||
*
|
||||
* It exists because previously mozDownloadManager.clearAllDone() thought that
|
||||
* when it returned that all the completed downloads would be cleared out. It
|
||||
* was wrong and this led to various intermittent test failurse. In discussion
|
||||
* on https://bugzil.la/979446#c13 and onwards, it was decided that
|
||||
* clearAllDone() was in the wrong and that the jsdownloads API it depends on
|
||||
* was not going to change to make it be in the right.
|
||||
*
|
||||
* The existing uses of clearAllDone() in tests seemed to be about:
|
||||
* - Exploding if there was somehow still a download in progress
|
||||
* - Clearing out the download list at the start of a test so that calls to
|
||||
* getDownloads() wouldn't have to worry about existing downloads, etc.
|
||||
*
|
||||
* From discussion, the right way to handle clearing is to wait for the expected
|
||||
* removal events to occur for the existing downloads. So that's what we do.
|
||||
* We still generate a test failure if there are any in-progress downloads.
|
||||
*
|
||||
* @param {Boolean} [getDownloads=false]
|
||||
* If true, invoke getDownloads after clearing the download list and return
|
||||
* its value.
|
||||
*/
|
||||
function clearAllDoneHelper(getDownloads) {
|
||||
var clearedPromise = new Promise(function(resolve, reject) {
|
||||
function gotDownloads(downloads) {
|
||||
// If there are no downloads, we're already done.
|
||||
if (downloads.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Track the set of expected downloads that will be finalized.
|
||||
var expectedIds = new Set();
|
||||
function changeHandler(evt) {
|
||||
var download = evt.download;
|
||||
if (download.state === "finalized") {
|
||||
expectedIds.delete(download.id);
|
||||
if (expectedIds.size === 0) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
downloads.forEach(function(download) {
|
||||
if (download.state === "downloading") {
|
||||
ok(false, "A download is still active: " + download.path);
|
||||
reject("Active download");
|
||||
}
|
||||
download.onstatechange = changeHandler;
|
||||
expectedIds.add(download.id);
|
||||
});
|
||||
navigator.mozDownloadManager.clearAllDone();
|
||||
}
|
||||
function gotBadNews(err) {
|
||||
ok(false, "Problem clearing all downloads: " + err);
|
||||
reject(err);
|
||||
}
|
||||
navigator.mozDownloadManager.getDownloads().then(gotDownloads, gotBadNews);
|
||||
});
|
||||
if (!getDownloads) {
|
||||
return clearedPromise;
|
||||
}
|
||||
return clearedPromise.then(function() {
|
||||
return navigator.mozDownloadManager.getDownloads();
|
||||
});
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
[DEFAULT]
|
||||
# The actual requirement for mozDownloadManager is MOZ_GONK because of
|
||||
# the nsIVolumeService dependency. Until https://bugzil.la/1130264 is
|
||||
# addressed, there is no way for mulet to run these tests.
|
||||
run-if = toolkit == 'gonk'
|
||||
support-files =
|
||||
serve_file.sjs
|
||||
clear_all_done_helper.js
|
||||
|
||||
[test_downloads_navigator_object.html]
|
||||
[test_downloads_basic.html]
|
||||
[test_downloads_large.html]
|
||||
[test_downloads_bad_file.html]
|
||||
[test_downloads_pause_remove.html]
|
||||
[test_downloads_pause_resume.html]
|
@ -1,170 +0,0 @@
|
||||
// Serves a file with a given mime type and size at an optionally given rate.
|
||||
|
||||
function getQuery(request) {
|
||||
var query = {};
|
||||
request.queryString.split('&').forEach(function (val) {
|
||||
var [name, value] = val.split('=');
|
||||
query[name] = unescape(value);
|
||||
});
|
||||
return query;
|
||||
}
|
||||
|
||||
function handleResponse() {
|
||||
// Is this a rate limited response?
|
||||
if (this.state.rate > 0) {
|
||||
// Calculate how many bytes we have left to send.
|
||||
var bytesToWrite = this.state.totalBytes - this.state.sentBytes;
|
||||
|
||||
// Do we have any bytes left to send? If not we'll just fall thru and
|
||||
// cancel our repeating timer and finalize the response.
|
||||
if (bytesToWrite > 0) {
|
||||
// Figure out how many bytes to send, based on the rate limit.
|
||||
bytesToWrite =
|
||||
(bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite;
|
||||
|
||||
for (let i = 0; i < bytesToWrite; i++) {
|
||||
try {
|
||||
this.response.bodyOutputStream.write("0", 1);
|
||||
} catch (e) {
|
||||
// Connection was closed by client.
|
||||
if (e == Components.results.NS_ERROR_NOT_AVAILABLE) {
|
||||
// There's no harm in calling this multiple times.
|
||||
this.response.finish();
|
||||
|
||||
// It's possible that our timer wasn't cancelled in time
|
||||
// and we'll be called again.
|
||||
if (this.timer) {
|
||||
this.timer.cancel();
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the number of bytes we've sent to the client.
|
||||
this.state.sentBytes += bytesToWrite;
|
||||
|
||||
// Wait until the next call to do anything else.
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not rate limited, write it all out.
|
||||
for (let i = 0; i < this.state.totalBytes; i++) {
|
||||
this.response.write("0");
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize the response.
|
||||
this.response.finish();
|
||||
|
||||
// All done sending, go ahead and cancel our repeating timer.
|
||||
this.timer.cancel();
|
||||
|
||||
// Clear the timer.
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
function handleRequest(request, response) {
|
||||
var query = getQuery(request);
|
||||
|
||||
// sending at a specific rate requires our response to be asynchronous so
|
||||
// we handle all requests asynchronously. See handleResponse().
|
||||
response.processAsync();
|
||||
|
||||
// Default status when responding.
|
||||
var version = "1.1";
|
||||
var statusCode = 200;
|
||||
var description = "OK";
|
||||
|
||||
// Default values for content type, size and rate.
|
||||
var contentType = "text/plain";
|
||||
var contentRange = null;
|
||||
var size = 1024;
|
||||
var rate = 0;
|
||||
|
||||
// optional content type to be used by our response.
|
||||
if ("contentType" in query) {
|
||||
contentType = query["contentType"];
|
||||
}
|
||||
|
||||
// optional size (in bytes) for generated file.
|
||||
if ("size" in query) {
|
||||
size = parseInt(query["size"]);
|
||||
}
|
||||
|
||||
// optional range request check.
|
||||
if (request.hasHeader("range")) {
|
||||
version = "1.1";
|
||||
statusCode = 206;
|
||||
description = "Partial Content";
|
||||
|
||||
// We'll only support simple range byte style requests.
|
||||
var [offset, total] = request.getHeader("range").slice("bytes=".length).split("-");
|
||||
// Enforce valid Number values.
|
||||
offset = parseInt(offset);
|
||||
offset = isNaN(offset) ? 0 : offset;
|
||||
// Same.
|
||||
total = parseInt(total);
|
||||
total = isNaN(total) ? 0 : total;
|
||||
|
||||
// We'll need to original total size as part of the Content-Range header
|
||||
// value in our response.
|
||||
var originalSize = size;
|
||||
|
||||
// If we have a total size requested, we must make sure to send that number
|
||||
// of bytes only (minus the start offset).
|
||||
if (total && total < size) {
|
||||
size = total - offset;
|
||||
} else if (offset) {
|
||||
// Looks like we just have a byte offset to deal with.
|
||||
size = size - offset;
|
||||
}
|
||||
|
||||
// We specifically need to add a Content-Range header to all responses for
|
||||
// requests that include a range request header.
|
||||
contentRange = "bytes " + offset + "-" + (size - 1) + "/" + originalSize;
|
||||
}
|
||||
|
||||
// optional rate (in bytes/s) at which to send the file.
|
||||
if ("rate" in query) {
|
||||
rate = parseInt(query["rate"]);
|
||||
}
|
||||
|
||||
// The context for the responseHandler.
|
||||
var context = {
|
||||
response: response,
|
||||
state: {
|
||||
contentType: contentType,
|
||||
totalBytes: size,
|
||||
sentBytes: 0,
|
||||
rate: rate
|
||||
},
|
||||
timer: null
|
||||
};
|
||||
|
||||
// The notify implementation for the timer.
|
||||
context.notify = handleResponse.bind(context);
|
||||
|
||||
context.timer =
|
||||
Components.classes["@mozilla.org/timer;1"]
|
||||
.createInstance(Components.interfaces.nsITimer);
|
||||
|
||||
// generate the content.
|
||||
response.setStatusLine(version, statusCode, description);
|
||||
response.setHeader("Content-Type", contentType, false);
|
||||
if (contentRange) {
|
||||
response.setHeader("Content-Range", contentRange, false);
|
||||
}
|
||||
response.setHeader("Content-Length", size.toString(), false);
|
||||
|
||||
// initialize the timer and start writing out the response.
|
||||
context.timer.initWithCallback(
|
||||
context,
|
||||
1000,
|
||||
Components.interfaces.nsITimer.TYPE_REPEATING_SLACK
|
||||
);
|
||||
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=960749
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 960749 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=960749">Mozilla Bug 960749</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<a href="serve_file.sjs?contentType=application/octet-stream&size=1024" download=".<.EVIL.>\ / : * ? " |file.bin" id="download1">Download #1</a>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
// Testing a simple download, waiting for it to be done.
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
var expected = "_.EVIL.__ _ _ _ _ _ _file.bin";
|
||||
|
||||
function next() {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index]();
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
function checkTargetFilename(download) {
|
||||
ok(download.path.endsWith(expected),
|
||||
"Download path leaf name '" + download.path +
|
||||
"' should match '" + expected + "' filename.");
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
var download = evt.download;
|
||||
|
||||
if (download.state === "succeeded") {
|
||||
checkTargetFilename(download);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadStart(evt) {
|
||||
var download = evt.download;
|
||||
download.onstatechange = downloadChange;
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
// Setup the event listeners.
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
navigator.mozDownloadManager.ondownloadstart = downloadStart;
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
// Click on the <a download> to start the download.
|
||||
function() {
|
||||
document.getElementById("download1").click();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,128 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<a href="serve_file.sjs?contentType=application/octet-stream&size=1024" download="test.bin" id="download1">Download #1</a>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
// Testing a simple download, waiting for it to be done.
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
var todayDate = new Date();
|
||||
var baseServeURL = "http://mochi.test:8888/tests/dom/downloads/tests/";
|
||||
var lastKnownCurrentBytes = 0;
|
||||
|
||||
function next() {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index]();
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
function checkConsistentDownloadAttributes(download) {
|
||||
var href = document.getElementById("download1").getAttribute("href");
|
||||
var expectedServeURL = baseServeURL + href;
|
||||
var destinationRegEx = /test\(?[0-9]*\)?\.bin$/;
|
||||
|
||||
// bug 945323: Download path isn't honoring download attribute
|
||||
ok(destinationRegEx.test(download.path),
|
||||
"Download path '" + download.path +
|
||||
"' should match '" + destinationRegEx + "' regexp.");
|
||||
|
||||
ok(download.startTime >= todayDate,
|
||||
"Download start time should be greater than or equal to today");
|
||||
|
||||
is(download.error, null, "Download does not have an error");
|
||||
|
||||
is(download.url, expectedServeURL,
|
||||
"Download URL = " + expectedServeURL);
|
||||
ok(download.id !== null, "Download id is defined");
|
||||
is(download.contentType, "application/octet-stream",
|
||||
"Download content type is application/octet-stream");
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
var download = evt.download;
|
||||
checkConsistentDownloadAttributes(download);
|
||||
is(download.totalBytes, 1024, "Download total size is 1024 bytes");
|
||||
|
||||
if (download.state === "succeeded") {
|
||||
is(download.currentBytes, 1024, "Download current size is 1024 bytes");
|
||||
SimpleTest.finish();
|
||||
} else if (download.state === "downloading") {
|
||||
// Note that this case may or may not trigger, depending on whether the
|
||||
// download is initially reported with 0 bytes (we should happen) or with
|
||||
// 1024 bytes (we should not happen). If we do happen, an additional 8
|
||||
// TEST-PASS events should be logged.
|
||||
ok(download.currentBytes > lastKnownCurrentBytes,
|
||||
"Download current size is larger than last download change event");
|
||||
lastKnownCurrentBytes = download.currentBytes;
|
||||
} else {
|
||||
ok(false, "Unexpected download state = " + download.state);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadStart(evt) {
|
||||
var download = evt.download;
|
||||
checkConsistentDownloadAttributes(download);
|
||||
|
||||
// We used to check that the currentBytes was 0. This was incorrect. It
|
||||
// is very common to first hear about the download already at 1024 bytes.
|
||||
is(download.state, "downloading", "Download state is downloading");
|
||||
|
||||
download.onstatechange = downloadChange;
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
// Setup the event listeners.
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
navigator.mozDownloadManager.ondownloadstart = downloadStart;
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
// Click on the <a download> to start the download.
|
||||
function() {
|
||||
document.getElementById("download1").click();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400" download="test.bin" id="download1">Large Download</a>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
// Testing downloading a file, then checking getDownloads() and clearAllDone().
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
|
||||
function next(args) {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index](args);
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Catch all error function.
|
||||
function error() {
|
||||
ok(false, "API failure");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function getDownloads(downloads) {
|
||||
ok(downloads.length == 1, "One downloads after getDownloads");
|
||||
clearAllDoneHelper(true).then(clearAllDone, error);
|
||||
}
|
||||
|
||||
function clearAllDone(downloads) {
|
||||
ok(downloads.length == 0, "No downloads after clearAllDone");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
var download = evt.download;
|
||||
|
||||
if (download.state == "succeeded") {
|
||||
ok(download.totalBytes == 102400, "Download size is 100k bytes.");
|
||||
navigator.mozDownloadManager.getDownloads().then(getDownloads, error);
|
||||
}
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
// Setup permission and clear current list.
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
function(downloads) {
|
||||
ok(downloads.length == 0, "Start with an empty download list.");
|
||||
next();
|
||||
},
|
||||
|
||||
// Setup the event listeners.
|
||||
function() {
|
||||
navigator.mozDownloadManager.ondownloadstart =
|
||||
function(evt) {
|
||||
ok(true, "Download started");
|
||||
evt.download.addEventListener("statechange", downloadChange);
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
// Click on the <a download> to start the download.
|
||||
function() {
|
||||
document.getElementById("download1").click();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -1,75 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
<iframe></iframe>
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
|
||||
function next() {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index]();
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: 0, context: document}
|
||||
], function() {
|
||||
is(frames[0].navigator.mozDownloadManager, null, "navigator.mozDownloadManager is null when the page doesn't have permissions");
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", false]]
|
||||
}, function() {
|
||||
is(navigator.mozDownloadManager, undefined, "navigator.mozDownloadManager is undefined");
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function() {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -1,117 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
// Testing pausing a download and then removing it.
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
|
||||
function next(args) {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index](args);
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var pausing = false;
|
||||
|
||||
// Catch all error function.
|
||||
function error() {
|
||||
ok(false, "API failure");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function checkDownloadList(downloads) {
|
||||
ok(downloads.length == 0, "No downloads left");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function checkRemoved(download) {
|
||||
ok(download.state == "finalized", "Download removed.");
|
||||
navigator.mozDownloadManager.getDownloads()
|
||||
.then(checkDownloadList, error);
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
var download = evt.download;
|
||||
|
||||
if (download.state == "downloading" && !pausing) {
|
||||
pausing = true;
|
||||
download.pause();
|
||||
} else if (download.state == "stopped") {
|
||||
ok(pausing, "Download stopped by pause()");
|
||||
navigator.mozDownloadManager.remove(download)
|
||||
.then(checkRemoved, error);
|
||||
}
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
// Setup permission and clear current list.
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
function(downloads) {
|
||||
ok(downloads.length == 0, "Start with an empty download list.");
|
||||
next();
|
||||
},
|
||||
|
||||
// Setup the event listeners.
|
||||
function() {
|
||||
navigator.mozDownloadManager.ondownloadstart =
|
||||
function(evt) {
|
||||
ok(true, "Download started");
|
||||
evt.download.addEventListener("statechange", downloadChange);
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
// Click on the <a download> to start the download.
|
||||
function() {
|
||||
document.getElementById("download1").click();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -1,121 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
// Testing pausing a download and then resuming it.
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var index = -1;
|
||||
|
||||
function next(args) {
|
||||
index += 1;
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
steps[index](args);
|
||||
} catch(ex) {
|
||||
ok(false, "Caught exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var pausing = false;
|
||||
var resuming = false;
|
||||
|
||||
// Catch all error function.
|
||||
function error() {
|
||||
ok(false, "API failure");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function checkDownloadList(downloads) {
|
||||
ok(downloads.length == 0, "No downloads left");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function checkResumeSucceeded(download) {
|
||||
ok(download.state == "succeeded", "Download resumed successfully.");
|
||||
clearAllDoneHelper(true).then(checkDownloadList, error);
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
var download = evt.download;
|
||||
|
||||
info("got download event, state: " + download.state +
|
||||
" current bytes: " + download.currentBytes +
|
||||
" pausing?: " + pausing + " resuming?: " + resuming);
|
||||
if (download.state == "downloading" && !pausing) {
|
||||
pausing = true;
|
||||
download.pause();
|
||||
} else if (download.state == "stopped" && !resuming) {
|
||||
resuming = true;
|
||||
ok(pausing, "Download stopped by pause()");
|
||||
download.resume()
|
||||
.then(function() { checkResumeSucceeded(download); }, error);
|
||||
}
|
||||
}
|
||||
|
||||
var steps = [
|
||||
// Start by setting the pref to true.
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}, next);
|
||||
},
|
||||
|
||||
// Setup permission and clear current list.
|
||||
function() {
|
||||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
function(downloads) {
|
||||
ok(downloads.length == 0, "Start with an empty download list.");
|
||||
next();
|
||||
},
|
||||
|
||||
// Setup the event listeners.
|
||||
function() {
|
||||
navigator.mozDownloadManager.ondownloadstart =
|
||||
function(evt) {
|
||||
ok(true, "Download started");
|
||||
evt.download.addEventListener("statechange", downloadChange);
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
// Click on the <a download> to start the download.
|
||||
function() {
|
||||
document.getElementById("download1").click();
|
||||
}
|
||||
];
|
||||
|
||||
next();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -23,7 +23,6 @@
|
||||
#include "nsIDocument.h"
|
||||
#include "nsIDOMDocument.h"
|
||||
#include "nsIDOMRange.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIFrame.h"
|
||||
#include "nsINode.h"
|
||||
#include "nsIPresShell.h"
|
||||
@ -642,12 +641,11 @@ IMEContentObserver::IsEditorComposing() const
|
||||
// whether the editor already started to handle composition because
|
||||
// web contents can change selection, text content and/or something from
|
||||
// compositionstart event listener which is run before EditorBase handles it.
|
||||
nsCOMPtr<nsIEditorIMESupport> editorIMESupport = do_QueryInterface(mEditor);
|
||||
if (NS_WARN_IF(!editorIMESupport)) {
|
||||
if (NS_WARN_IF(!mEditor)) {
|
||||
return false;
|
||||
}
|
||||
bool isComposing = false;
|
||||
nsresult rv = editorIMESupport->GetComposing(&isComposing);
|
||||
nsresult rv = mEditor->GetComposing(&isComposing);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return false;
|
||||
}
|
||||
|
@ -73,22 +73,15 @@ function triggerShortAnimation(node) {
|
||||
node.style.animation = "anim1 1ms linear";
|
||||
}
|
||||
|
||||
// This function triggers a long animation with two iterations, which is
|
||||
// *nearly* at the end of its first iteration. It will hit the end of that
|
||||
// iteration (firing an event) almost immediately, 1ms in the future.
|
||||
// This function triggers a very short (10ms long) animation with many
|
||||
// iterations, which will cause a start event followed by an iteration event
|
||||
// on each subsequent tick, to fire.
|
||||
//
|
||||
// NOTE: It's important that this animation have a *long* duration. If it were
|
||||
// short (e.g. 1ms duration), then we might jump past all its iterations in
|
||||
// a single refresh-driver tick. And if that were to happens, we'd *never* fire
|
||||
// any animationiteration events -- the CSS Animations spec says this event
|
||||
// must not be fired "...when an animationend event would fire at the same time"
|
||||
// (which would be the case in this example with a 1ms duration). So, to make
|
||||
// sure our event does fire, we use a long duration and a nearly-as-long
|
||||
// negative delay. This ensures we hit the end of the first iteration right
|
||||
// away, and that we don't risk hitting the end of the second iteration at the
|
||||
// same time.
|
||||
// NOTE: We need the many iterations since if an animation frame coincides
|
||||
// with the animation starting or ending we dispatch only the start or end
|
||||
// event and not the iteration event.
|
||||
function triggerAnimationIteration(node) {
|
||||
node.style.animation = "anim1 300s -299.999s linear 2";
|
||||
node.style.animation = "anim1 10ms linear 20000";
|
||||
}
|
||||
|
||||
// GENERAL UTILITY FUNCTIONS
|
||||
|
@ -77,7 +77,6 @@
|
||||
#include "nsDOMStringMap.h"
|
||||
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsLayoutUtils.h"
|
||||
#include "mozAutoDocUpdate.h"
|
||||
#include "nsHtml5Module.h"
|
||||
@ -1829,11 +1828,8 @@ nsGenericHTMLFormElement::GetDesiredIMEState()
|
||||
nsIEditor* editor = GetEditorInternal();
|
||||
if (!editor)
|
||||
return nsGenericHTMLElement::GetDesiredIMEState();
|
||||
nsCOMPtr<nsIEditorIMESupport> imeEditor = do_QueryInterface(editor);
|
||||
if (!imeEditor)
|
||||
return nsGenericHTMLElement::GetDesiredIMEState();
|
||||
IMEState state;
|
||||
nsresult rv = imeEditor->GetPreferredIMEState(&state);
|
||||
nsresult rv = editor->GetPreferredIMEState(&state);
|
||||
if (NS_FAILED(rv))
|
||||
return nsGenericHTMLElement::GetDesiredIMEState();
|
||||
return state;
|
||||
|
@ -27,7 +27,6 @@
|
||||
#include "nsAttrValueInlines.h"
|
||||
#include "nsGenericHTMLElement.h"
|
||||
#include "nsIDOMEventListener.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIEditorObserver.h"
|
||||
#include "nsIWidget.h"
|
||||
#include "nsIDocumentEncoder.h"
|
||||
@ -2017,10 +2016,7 @@ nsTextEditorState::SetValue(const nsAString& aValue, uint32_t aFlags)
|
||||
// document may be unloaded.
|
||||
mValueBeingSet = aValue;
|
||||
mIsCommittingComposition = true;
|
||||
nsCOMPtr<nsIEditorIMESupport> editorIMESupport =
|
||||
do_QueryInterface(mEditor);
|
||||
MOZ_RELEASE_ASSERT(editorIMESupport);
|
||||
nsresult rv = editorIMESupport->ForceCompositionEnd();
|
||||
nsresult rv = mEditor->ForceCompositionEnd();
|
||||
if (!self.get()) {
|
||||
return true;
|
||||
}
|
||||
@ -2284,9 +2280,8 @@ bool
|
||||
nsTextEditorState::EditorHasComposition()
|
||||
{
|
||||
bool isComposing = false;
|
||||
nsCOMPtr<nsIEditorIMESupport> editorIMESupport = do_QueryInterface(mEditor);
|
||||
return editorIMESupport &&
|
||||
NS_SUCCEEDED(editorIMESupport->GetComposing(&isComposing)) &&
|
||||
return mEditor &&
|
||||
NS_SUCCEEDED(mEditor->GetComposing(&isComposing)) &&
|
||||
isComposing;
|
||||
}
|
||||
|
||||
|
@ -107,11 +107,6 @@ if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
if CONFIG['MOZ_SECUREELEMENT']:
|
||||
DIRS += ['secureelement']
|
||||
|
||||
if CONFIG['MOZ_B2G']:
|
||||
DIRS += [
|
||||
'downloads',
|
||||
]
|
||||
|
||||
DIRS += ['presentation']
|
||||
|
||||
TEST_DIRS += [
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "nsXULAppAPI.h"
|
||||
#include "nsEscape.h"
|
||||
#include "nsNetCID.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "nsServiceManagerUtils.h"
|
||||
|
||||
@ -31,6 +32,8 @@ namespace dom {
|
||||
static const char kStartupTopic[] = "sessionstore-windows-restored";
|
||||
static const uint32_t kStartupDelay = 0;
|
||||
|
||||
const char kTestingPref[] = "dom.storage.testing";
|
||||
|
||||
NS_IMPL_ISUPPORTS(DOMStorageObserver,
|
||||
nsIObserver,
|
||||
nsISupportsWeakReference)
|
||||
@ -72,15 +75,9 @@ DOMStorageObserver::Init()
|
||||
// Observe low device storage notifications.
|
||||
obs->AddObserver(sSelf, "disk-space-watcher", true);
|
||||
|
||||
#ifdef DOM_STORAGE_TESTS
|
||||
// Testing
|
||||
obs->AddObserver(sSelf, "domstorage-test-flush-force", true);
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Only to forward to child process.
|
||||
obs->AddObserver(sSelf, "domstorage-test-flushed", true);
|
||||
}
|
||||
|
||||
obs->AddObserver(sSelf, "domstorage-test-reload", true);
|
||||
#ifdef DOM_STORAGE_TESTS
|
||||
Preferences::RegisterCallbackAndCall(TestingPrefChanged, kTestingPref);
|
||||
#endif
|
||||
|
||||
return NS_OK;
|
||||
@ -98,6 +95,32 @@ DOMStorageObserver::Shutdown()
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
void
|
||||
DOMStorageObserver::TestingPrefChanged(const char* aPrefName, void* aClosure)
|
||||
{
|
||||
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
||||
if (!obs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences::GetBool(kTestingPref)) {
|
||||
obs->AddObserver(sSelf, "domstorage-test-flush-force", true);
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Only to forward to child process.
|
||||
obs->AddObserver(sSelf, "domstorage-test-flushed", true);
|
||||
}
|
||||
obs->AddObserver(sSelf, "domstorage-test-reload", true);
|
||||
} else {
|
||||
obs->RemoveObserver(sSelf, "domstorage-test-flush-force");
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Only to forward to child process.
|
||||
obs->RemoveObserver(sSelf, "domstorage-test-flushed");
|
||||
}
|
||||
obs->RemoveObserver(sSelf, "domstorage-test-reload");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DOMStorageObserver::AddSink(DOMStorageObserverSink* aObs)
|
||||
{
|
||||
|
@ -54,6 +54,8 @@ public:
|
||||
private:
|
||||
virtual ~DOMStorageObserver() {}
|
||||
|
||||
static void TestingPrefChanged(const char* aPrefName, void* aClosure);
|
||||
|
||||
static DOMStorageObserver* sSelf;
|
||||
|
||||
// Weak references
|
||||
|
@ -43,3 +43,11 @@ function notify(top)
|
||||
{
|
||||
os().notifyObservers(null, top, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable testing observer notifications in DOMStorageObserver.cpp.
|
||||
*/
|
||||
function localStorageEnableTestingMode(cb)
|
||||
{
|
||||
SpecialPowers.pushPrefEnv({ "set": [["dom.storage.testing", true]] }, cb);
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ Case 3: set | set | clear | reload | count ?= 0
|
||||
|
||||
function startTest()
|
||||
{
|
||||
// Enable testing observer notifications
|
||||
localStorageEnableTestingMode(function() {
|
||||
|
||||
// Have an untouched land
|
||||
localStorage.clear();
|
||||
|
||||
@ -153,6 +156,7 @@ function startTest()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
@ -3,7 +3,6 @@
|
||||
<title>Test localStorage usage while in a low device storage situation</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="localStorageCommon.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@ -23,7 +22,7 @@ This test does the following:
|
||||
|
||||
function lowDeviceStorage(lowStorage) {
|
||||
var data = lowStorage ? "full" : "free";
|
||||
os().notifyObservers(null, "disk-space-watcher", data);
|
||||
SpecialPowers.Services.obs.notifyObservers(null, "disk-space-watcher", data);
|
||||
}
|
||||
|
||||
function startTest() {
|
||||
|
@ -1,92 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {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/DOMRequestHelper.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
|
||||
"@mozilla.org/childprocessmessagemanager;1",
|
||||
"nsIMessageSender");
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
const TETHERING_TYPE_WIFI = "wifi";
|
||||
const TETHERING_TYPE_BLUETOOTH = "bt";
|
||||
const TETHERING_TYPE_USB = "usb";
|
||||
|
||||
function TetheringManager() {
|
||||
}
|
||||
|
||||
TetheringManager.prototype = {
|
||||
__proto__: DOMRequestIpcHelper.prototype,
|
||||
|
||||
classDescription: "TetheringManager",
|
||||
classID: Components.ID("{bd8a831c-d8ec-4f00-8803-606e50781097}"),
|
||||
contractID: "@mozilla.org/dom/tetheringmanager;1",
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIObserver]),
|
||||
|
||||
init: function(aWindow) {
|
||||
const messages = ["WifiManager:setWifiTethering:Return:OK",
|
||||
"WifiManager:setWifiTethering:Return:NO"];
|
||||
this.initDOMRequestHelper(aWindow, messages);
|
||||
},
|
||||
|
||||
// TODO : aMessage format may be different after supporting bt/usb.
|
||||
// for now, use wifi format first.
|
||||
receiveMessage: function(aMessage) {
|
||||
let data = aMessage.data.data;
|
||||
|
||||
let resolver = this.takePromiseResolver(data.resolverId);
|
||||
if (!resolver) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "WifiManager:setWifiTethering:Return:OK":
|
||||
resolver.resolve(data);
|
||||
break;
|
||||
case "WifiManager:setWifiTethering:Return:NO":
|
||||
resolver.reject(data.reason);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
setTetheringEnabled: function setTetheringEnabled(aEnabled, aType, aConfig) {
|
||||
let self = this;
|
||||
switch (aType) {
|
||||
case TETHERING_TYPE_WIFI:
|
||||
return this.createPromiseWithId(function(aResolverId) {
|
||||
let data = { resolverId: aResolverId, enabled: aEnabled, config: aConfig };
|
||||
cpmm.sendAsyncMessage("WifiManager:setWifiTethering", { data: data});
|
||||
});
|
||||
case TETHERING_TYPE_BLUETOOTH:
|
||||
case TETHERING_TYPE_USB:
|
||||
default:
|
||||
debug("tethering type(" + aType + ") doesn't support");
|
||||
return this.createPromiseWithId(function(aResolverId) {
|
||||
self.takePromiseResolver(aResolverId).reject();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory =
|
||||
XPCOMUtils.generateNSGetFactory([TetheringManager]);
|
||||
|
||||
var debug;
|
||||
if (DEBUG) {
|
||||
debug = function (s) {
|
||||
dump("-*- TetheringManager component: " + s + "\n");
|
||||
};
|
||||
} else {
|
||||
debug = function (s) {};
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
#TetheringManager.js
|
||||
component {bd8a831c-d8ec-4f00-8803-606e50781097} TetheringManager.js
|
||||
contract @mozilla.org/tetheringmanager;1 {bd8a831c-d8ec-4f00-8803-606e50781097}
|
||||
|
@ -1,12 +0,0 @@
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'TetheringManager.js',
|
||||
'TetheringManager.manifest',
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
@ -1,768 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
const TYPE_WIFI = "wifi";
|
||||
const TYPE_BLUETOOTH = "bt";
|
||||
const TYPE_USB = "usb";
|
||||
|
||||
/**
|
||||
* General tethering setting.
|
||||
*/
|
||||
const TETHERING_SETTING_IP = "192.168.1.1";
|
||||
const TETHERING_SETTNG_PREFIX = "24";
|
||||
const TETHERING_SETTING_START_IP = "192.168.1.10";
|
||||
const TETHERING_SETTING_END_IP = "192.168.1.30";
|
||||
const TETHERING_SETTING_DNS1 = "8.8.8.8";
|
||||
const TETHERING_SETTING_DNS2 = "8.8.4.4";
|
||||
|
||||
const TETHERING_NETWORK_ADDR = "192.168.1.0/24";
|
||||
|
||||
/**
|
||||
* Wifi tethering setting.
|
||||
*/
|
||||
const TETHERING_SETTING_SSID = "FirefoxHotSpot";
|
||||
const TETHERING_SETTING_SECURITY = "open";
|
||||
const TETHERING_SETTING_KEY = "1234567890";
|
||||
|
||||
const SETTINGS_RIL_DATA_ENABLED = 'ril.data.enabled';
|
||||
const SETTINGS_KEY_DATA_APN_SETTINGS = "ril.data.apnSettings";
|
||||
|
||||
// Emulate Promise.jsm semantics.
|
||||
Promise.defer = function() { return new Deferred(); }
|
||||
function Deferred() {
|
||||
this.promise = new Promise(function(resolve, reject) {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
}.bind(this));
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
var gTestSuite = (function() {
|
||||
let suite = {};
|
||||
|
||||
let tetheringManager;
|
||||
let pendingEmulatorShellCount = 0;
|
||||
|
||||
/**
|
||||
* A wrapper function of "is".
|
||||
*
|
||||
* Calls the marionette function "is" as well as throws an exception
|
||||
* if the givens values are not equal.
|
||||
*
|
||||
* @param value1
|
||||
* Any type of value to compare.
|
||||
*
|
||||
* @param value2
|
||||
* Any type of value to compare.
|
||||
*
|
||||
* @param message
|
||||
* Debug message for this check.
|
||||
*
|
||||
*/
|
||||
function isOrThrow(value1, value2, message) {
|
||||
is(value1, value2, message);
|
||||
if (value1 !== value2) {
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send emulator shell command with safe guard.
|
||||
*
|
||||
* We should only call |finish()| after all emulator command transactions
|
||||
* end, so here comes with the pending counter. Resolve when the emulator
|
||||
* gives positive response, and reject otherwise.
|
||||
*
|
||||
* Fulfill params:
|
||||
* result -- an array of emulator response lines.
|
||||
* Reject params:
|
||||
* result -- an array of emulator response lines.
|
||||
*
|
||||
* @param aCommand
|
||||
* A string command to be passed to emulator through its telnet console.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function runEmulatorShellSafe(aCommand) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
++pendingEmulatorShellCount;
|
||||
runEmulatorShell(aCommand, function(aResult) {
|
||||
--pendingEmulatorShellCount;
|
||||
|
||||
ok(true, "Emulator shell response: " + JSON.stringify(aResult));
|
||||
if (Array.isArray(aResult)) {
|
||||
deferred.resolve(aResult);
|
||||
} else {
|
||||
deferred.reject(aResult);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for timeout.
|
||||
*
|
||||
* Resolve when the given duration elapsed. Never reject.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
*
|
||||
* @param aTimeoutMs
|
||||
* The duration after which the timeout event should occurs.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function waitForTimeout(aTimeoutMs) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
setTimeout(function() {
|
||||
deferred.resolve();
|
||||
}, aTimeoutMs);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mozSettings value specified by @aKey.
|
||||
*
|
||||
* Resolve if that mozSettings value is retrieved successfully, reject
|
||||
* otherwise.
|
||||
*
|
||||
* Fulfill params:
|
||||
* The corresponding mozSettings value of the key.
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aKey
|
||||
* A string.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function getSettings(aKey) {
|
||||
let request = navigator.mozSettings.createLock().get(aKey);
|
||||
|
||||
return wrapDomRequestAsPromise(request)
|
||||
.then(function resolve(aEvent) {
|
||||
ok(true, "getSettings(" + aKey + ") - success");
|
||||
return aEvent.target.result[aKey];
|
||||
}, function reject(aEvent) {
|
||||
ok(false, "getSettings(" + aKey + ") - error");
|
||||
throw aEvent.target.error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mozSettings values.
|
||||
*
|
||||
* Resolve if that mozSettings value is set successfully, reject otherwise.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aSettings
|
||||
* An object of format |{key1: value1, key2: value2, ...}|.
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function setSettings(aSettings) {
|
||||
let lock = window.navigator.mozSettings.createLock();
|
||||
let request = lock.set(aSettings);
|
||||
let deferred = Promise.defer();
|
||||
lock.onsettingstransactionsuccess = function () {
|
||||
ok(true, "setSettings(" + JSON.stringify(aSettings) + ")");
|
||||
deferred.resolve();
|
||||
};
|
||||
lock.onsettingstransactionfailure = function (aEvent) {
|
||||
ok(false, "setSettings(" + JSON.stringify(aSettings) + ")");
|
||||
deferred.reject();
|
||||
throw aEvent.target.error;
|
||||
};
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mozSettings value with only one key.
|
||||
*
|
||||
* Resolve if that mozSettings value is set successfully, reject otherwise.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aKey
|
||||
* A string key.
|
||||
* @param aValue
|
||||
* An object value.
|
||||
* @param aAllowError [optional]
|
||||
* A boolean value. If set to true, an error response won't be treated
|
||||
* as test failure. Default: false.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function setSettings1(aKey, aValue, aAllowError) {
|
||||
let settings = {};
|
||||
settings[aKey] = aValue;
|
||||
return setSettings(settings, aAllowError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient MozSettings getter for SETTINGS_KEY_DATA_APN_SETTINGS.
|
||||
*/
|
||||
function getDataApnSettings(aAllowError) {
|
||||
return getSettings(SETTINGS_KEY_DATA_APN_SETTINGS, aAllowError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient MozSettings setter for SETTINGS_KEY_DATA_APN_SETTINGS.
|
||||
*/
|
||||
function setDataApnSettings(aApnSettings, aAllowError) {
|
||||
return setSettings1(SETTINGS_KEY_DATA_APN_SETTINGS, aApnSettings, aAllowError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 'ro.tethering.dun_required' system property to 1. Note that this is a
|
||||
* 'ro' property, it can only be set once.
|
||||
*/
|
||||
function setTetheringDunRequired() {
|
||||
return runEmulatorShellSafe(['setprop', 'ro.tethering.dun_required', '1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
|
||||
*
|
||||
* Fulfill params: A DOMEvent.
|
||||
* Reject params: A DOMEvent.
|
||||
*
|
||||
* @param aRequest
|
||||
* A DOMRequest instance.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function wrapDomRequestAsPromise(aRequest) {
|
||||
let deffered = Promise.defer();
|
||||
|
||||
ok(aRequest instanceof DOMRequest,
|
||||
"aRequest is instanceof" + aRequest.constructor);
|
||||
|
||||
aRequest.onsuccess = function(aEvent) {
|
||||
deffered.resolve(aEvent);
|
||||
};
|
||||
aRequest.onerror = function(aEvent) {
|
||||
deffered.reject(aEvent);
|
||||
};
|
||||
|
||||
return deffered.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for one named MozMobileConnection event.
|
||||
*
|
||||
* Resolve if that named event occurs. Never reject.
|
||||
*
|
||||
* Fulfill params: the DOMEvent passed.
|
||||
*
|
||||
* @param aEventName
|
||||
* A string event name.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function waitForMobileConnectionEventOnce(aEventName, aServiceId) {
|
||||
aServiceId = aServiceId || 0;
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let mobileconnection = navigator.mozMobileConnections[aServiceId];
|
||||
|
||||
mobileconnection.addEventListener(aEventName, function onevent(aEvent) {
|
||||
mobileconnection.removeEventListener(aEventName, onevent);
|
||||
|
||||
ok(true, "Mobile connection event '" + aEventName + "' got.");
|
||||
deferred.resolve(aEvent);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for RIL data being connected.
|
||||
*
|
||||
* This function will check |MozMobileConnection.data.connected| on
|
||||
* every 'datachange' event. Resolve when |MozMobileConnection.data.connected|
|
||||
* becomes the expected state. Never reject.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aConnected
|
||||
* Boolean that indicates the desired data state.
|
||||
*
|
||||
* @param aServiceId [optional]
|
||||
* A numeric DSDS service id. Default: 0.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function waitForRilDataConnected(aConnected, aServiceId) {
|
||||
aServiceId = aServiceId || 0;
|
||||
|
||||
return waitForMobileConnectionEventOnce('datachange', aServiceId)
|
||||
.then(function () {
|
||||
let mobileconnection = navigator.mozMobileConnections[aServiceId];
|
||||
if (mobileconnection.data.connected !== aConnected) {
|
||||
return waitForRilDataConnected(aConnected, aServiceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify everything about routing when the wifi tethering is either on or off.
|
||||
*
|
||||
* We use two unix commands to verify the routing: 'netcfg' and 'ip route'.
|
||||
* For now the following two things will be checked:
|
||||
* 1) The default route interface should be 'rmnet0'.
|
||||
* 2) wlan0 is up and its ip is set to a private subnet.
|
||||
*
|
||||
* We also verify iptables output as netd's NatController will execute
|
||||
* $ iptables -t nat -A POSTROUTING -o rmnet0 -j MASQUERADE
|
||||
*
|
||||
* For tethering through dun, we use 'ip rule' to find the secondary routing
|
||||
* table and look for default route on that table.
|
||||
*
|
||||
* Resolve when the verification is successful and reject otherwise.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: String that indicates the reason of rejection.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function verifyTetheringRouting(aEnabled, aIsDun) {
|
||||
let netcfgResult = {};
|
||||
let ipRouteResult = {};
|
||||
let ipSecondaryRouteResult = {};
|
||||
|
||||
// Execute 'netcfg' and parse to |netcfgResult|, each key of which is the
|
||||
// interface name and value is { ip(string) }.
|
||||
function exeAndParseNetcfg() {
|
||||
return runEmulatorShellSafe(['netcfg'])
|
||||
.then(function (aLines) {
|
||||
// Sample output:
|
||||
//
|
||||
// lo UP 127.0.0.1/8 0x00000049 00:00:00:00:00:00
|
||||
// eth0 UP 10.0.2.15/24 0x00001043 52:54:00:12:34:56
|
||||
// rmnet1 DOWN 0.0.0.0/0 0x00001002 52:54:00:12:34:58
|
||||
// rmnet2 DOWN 0.0.0.0/0 0x00001002 52:54:00:12:34:59
|
||||
// rmnet3 DOWN 0.0.0.0/0 0x00001002 52:54:00:12:34:5a
|
||||
// wlan0 UP 192.168.1.1/24 0x00001043 52:54:00:12:34:5b
|
||||
// sit0 DOWN 0.0.0.0/0 0x00000080 00:00:00:00:00:00
|
||||
// rmnet0 UP 10.0.2.100/24 0x00001043 52:54:00:12:34:57
|
||||
//
|
||||
aLines.forEach(function (aLine) {
|
||||
let tokens = aLine.split(/\s+/);
|
||||
if (tokens.length < 5) {
|
||||
return;
|
||||
}
|
||||
let ifname = tokens[0];
|
||||
let ip = (tokens[2].split('/'))[0];
|
||||
netcfgResult[ifname] = { ip: ip };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Execute 'ip route' and parse to |ipRouteResult|, each key of which is the
|
||||
// interface name and value is { src(string), default(boolean) }.
|
||||
function exeAndParseIpRoute() {
|
||||
return runEmulatorShellSafe(['ip', 'route'])
|
||||
.then(function (aLines) {
|
||||
// Sample output:
|
||||
//
|
||||
// 10.0.2.4 via 10.0.2.2 dev rmnet0
|
||||
// 10.0.2.3 via 10.0.2.2 dev rmnet0
|
||||
// 192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.1
|
||||
// 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15
|
||||
// 10.0.2.0/24 dev rmnet0 proto kernel scope link src 10.0.2.100
|
||||
// default via 10.0.2.2 dev rmnet0
|
||||
// default via 10.0.2.2 dev eth0 metric 2
|
||||
//
|
||||
|
||||
// Parse source ip for each interface.
|
||||
aLines.forEach(function (aLine) {
|
||||
let tokens = aLine.trim().split(/\s+/);
|
||||
let srcIndex = tokens.indexOf('src');
|
||||
if (srcIndex < 0 || srcIndex + 1 >= tokens.length) {
|
||||
return;
|
||||
}
|
||||
let ifname = tokens[2];
|
||||
let src = tokens[srcIndex + 1];
|
||||
ipRouteResult[ifname] = { src: src, default: false };
|
||||
});
|
||||
|
||||
// Parse default interfaces.
|
||||
aLines.forEach(function (aLine) {
|
||||
let tokens = aLine.split(/\s+/);
|
||||
if (tokens.length < 2) {
|
||||
return;
|
||||
}
|
||||
if ('default' === tokens[0]) {
|
||||
let ifnameIndex = tokens.indexOf('dev');
|
||||
if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
|
||||
return;
|
||||
}
|
||||
let ifname = tokens[ifnameIndex + 1];
|
||||
if (ipRouteResult[ifname]) {
|
||||
ipRouteResult[ifname].default = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Find MASQUERADE in POSTROUTING section. 'MASQUERADE' should be found
|
||||
// when tethering is enabled. 'MASQUERADE' shouldn't be found when tethering
|
||||
// is disabled.
|
||||
function verifyIptables() {
|
||||
return runEmulatorShellSafe(['iptables', '-t', 'nat', '-L', 'POSTROUTING'])
|
||||
.then(function(aLines) {
|
||||
// $ iptables -t nat -L POSTROUTING
|
||||
//
|
||||
// Sample output (tethering on):
|
||||
//
|
||||
// Chain POSTROUTING (policy ACCEPT)
|
||||
// target prot opt source destination
|
||||
// MASQUERADE all -- anywhere anywhere
|
||||
//
|
||||
let found = (function find_MASQUERADE() {
|
||||
// Skip first two lines.
|
||||
for (let i = 2; i < aLines.length; i++) {
|
||||
if (-1 !== aLines[i].indexOf('MASQUERADE')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if ((aEnabled && !found) || (!aEnabled && found)) {
|
||||
throw 'MASQUERADE' + (found ? '' : ' not') + ' found while tethering is ' +
|
||||
(aEnabled ? 'enabled' : 'disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Execute 'ip rule show', there must be one rule for tethering network
|
||||
// address to lookup for a secondary routing table, return that table id.
|
||||
function verifyIpRule() {
|
||||
if (!aIsDun) {
|
||||
return;
|
||||
}
|
||||
|
||||
return runEmulatorShellSafe(['ip', 'rule', 'show'])
|
||||
.then(function (aLines) {
|
||||
// Sample output:
|
||||
//
|
||||
// 0: from all lookup local
|
||||
// 32765: from 192.168.1.0/24 lookup 60
|
||||
// 32766: from all lookup main
|
||||
// 32767: from all lookup default
|
||||
//
|
||||
let tableId = (function findTableId() {
|
||||
for (let i = 0; i < aLines.length; i++) {
|
||||
let tokens = aLines[i].split(/\s+/);
|
||||
if (-1 != tokens.indexOf(TETHERING_NETWORK_ADDR)) {
|
||||
let lookupIndex = tokens.indexOf('lookup');
|
||||
if (lookupIndex < 0 || lookupIndex + 1 >= tokens.length) {
|
||||
return;
|
||||
}
|
||||
return tokens[lookupIndex + 1];
|
||||
}
|
||||
}
|
||||
return;
|
||||
})();
|
||||
|
||||
if ((aEnabled && !tableId) || (!aEnabled && tableId)) {
|
||||
throw 'Secondary table' + (tableId ? '' : ' not') + ' found while tethering is ' +
|
||||
(aEnabled ? 'enabled' : 'disabled');
|
||||
}
|
||||
|
||||
return tableId;
|
||||
});
|
||||
}
|
||||
|
||||
// Given the table id, use 'ip rule show table <table id>' to find the
|
||||
// default route on that secondary routing table.
|
||||
function execAndParseSecondaryTable(aTableId) {
|
||||
if (!aIsDun || !aEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return runEmulatorShellSafe(['ip', 'route', 'show', 'table', aTableId])
|
||||
.then(function (aLines) {
|
||||
// We only look for default route in secondary table.
|
||||
aLines.forEach(function (aLine) {
|
||||
let tokens = aLine.split(/\s+/);
|
||||
if (tokens.length < 2) {
|
||||
return;
|
||||
}
|
||||
if ('default' === tokens[0]) {
|
||||
let ifnameIndex = tokens.indexOf('dev');
|
||||
if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
|
||||
return;
|
||||
}
|
||||
let ifname = tokens[ifnameIndex + 1];
|
||||
ipSecondaryRouteResult[ifname] = { default: true };
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDefaultRouteAndIp(aExpectedWifiTetheringIp) {
|
||||
log(JSON.stringify(ipRouteResult));
|
||||
log(JSON.stringify(ipSecondaryRouteResult));
|
||||
log(JSON.stringify(netcfgResult));
|
||||
|
||||
if (aEnabled) {
|
||||
isOrThrow(ipRouteResult['rmnet0'].src, netcfgResult['rmnet0'].ip, 'rmnet0.ip');
|
||||
isOrThrow(ipRouteResult['rmnet0'].default, true, 'rmnet0.default');
|
||||
|
||||
isOrThrow(ipRouteResult['wlan0'].src, netcfgResult['wlan0'].ip, 'wlan0.ip');
|
||||
isOrThrow(ipRouteResult['wlan0'].src, aExpectedWifiTetheringIp, 'expected ip');
|
||||
isOrThrow(ipRouteResult['wlan0'].default, false, 'wlan0.default');
|
||||
|
||||
if (aIsDun) {
|
||||
isOrThrow(ipRouteResult['rmnet1'].src, netcfgResult['rmnet1'].ip, 'rmnet1.ip');
|
||||
isOrThrow(ipRouteResult['rmnet1'].default, false, 'rmnet1.default');
|
||||
// Dun's network default route is set on secondary routing table.
|
||||
isOrThrow(ipSecondaryRouteResult['rmnet1'].default, true, 'secondary rmnet1.default');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return verifyIptables()
|
||||
.then(verifyIpRule)
|
||||
.then(tableId => execAndParseSecondaryTable(tableId))
|
||||
.then(exeAndParseNetcfg)
|
||||
.then(exeAndParseIpRoute)
|
||||
.then(() => verifyDefaultRouteAndIp(TETHERING_SETTING_IP));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to enable/disable wifi tethering.
|
||||
*
|
||||
* Enable/disable wifi tethering by using setTetheringEnabled API
|
||||
* Resolve when the routing is verified to set up successfully in 20 seconds. The polling
|
||||
* period is 1 second.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: The error message.
|
||||
*
|
||||
* @param aEnabled
|
||||
* Boolean that indicates to enable or disable wifi tethering.
|
||||
* @param aIsDun
|
||||
* Boolean that indicates whether dun is required.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function setWifiTetheringEnabled(aEnabled, aIsDun) {
|
||||
let RETRY_INTERVAL_MS = 1000;
|
||||
let retryCnt = 20;
|
||||
|
||||
let config = {
|
||||
"ip" : TETHERING_SETTING_IP,
|
||||
"prefix" : TETHERING_SETTNG_PREFIX,
|
||||
"startIp" : TETHERING_SETTING_START_IP,
|
||||
"endIp" : TETHERING_SETTING_END_IP,
|
||||
"dns1" : TETHERING_SETTING_DNS1,
|
||||
"dns2" : TETHERING_SETTING_DNS2,
|
||||
"wifiConfig": {
|
||||
"ssid" : TETHERING_SETTING_SSID,
|
||||
"security" : TETHERING_SETTING_SECURITY
|
||||
}
|
||||
};
|
||||
|
||||
return tetheringManager.setTetheringEnabled(aEnabled, TYPE_WIFI, config)
|
||||
.then(function waitForRoutingVerified() {
|
||||
return verifyTetheringRouting(aEnabled, aIsDun)
|
||||
.then(null, function onreject(aReason) {
|
||||
|
||||
log('verifyTetheringRouting rejected due to ' + aReason +
|
||||
' (' + retryCnt + ')');
|
||||
|
||||
if (!retryCnt--) {
|
||||
throw aReason;
|
||||
}
|
||||
|
||||
return waitForTimeout(RETRY_INTERVAL_MS).then(waitForRoutingVerified);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure wifi is enabled/disabled.
|
||||
*
|
||||
* Issue a wifi enable/disable request if wifi is not in the desired state;
|
||||
* return a resolved promise otherwise.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @return a resolved promise or deferred promise.
|
||||
*/
|
||||
function ensureWifiEnabled(aEnabled) {
|
||||
let wifiManager = window.navigator.mozWifiManager;
|
||||
if (wifiManager.enabled === aEnabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
let request = wifiManager.setWifiEnabled(aEnabled);
|
||||
return wrapDomRequestAsPromise(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure tethering manager exists.
|
||||
*
|
||||
* Check navigator property |mozTetheringManager| to ensure we could access
|
||||
* tethering related function.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function ensureTetheringManager() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
tetheringManager = window.navigator.mozTetheringManager;
|
||||
|
||||
if (tetheringManager instanceof MozTetheringManager) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
log("navigator.mozTetheringManager is unavailable");
|
||||
deferred.reject();
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add required permissions for tethering. Never reject.
|
||||
*
|
||||
* The permissions required for wifi testing are 'wifi-manage' and 'settings-write'.
|
||||
* Never reject.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function acquirePermission() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let permissions = [{ 'type': 'wifi-manage', 'allow': 1, 'context': window.document },
|
||||
{ 'type': 'settings-write', 'allow': 1, 'context': window.document },
|
||||
{ 'type': 'settings-read', 'allow': 1, 'context': window.document },
|
||||
{ 'type': 'settings-api-write', 'allow': 1, 'context': window.document },
|
||||
{ 'type': 'settings-api-read', 'allow': 1, 'context': window.document },
|
||||
{ 'type': 'mobileconnection', 'allow': 1, 'context': window.document }];
|
||||
|
||||
SpecialPowers.pushPermissions(permissions, function() {
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common test routine.
|
||||
*
|
||||
* Start a test with the given test case chain. The test environment will be
|
||||
* settled down before the test. After the test, all the affected things will
|
||||
* be restored.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aTestCaseChain
|
||||
* The test case entry point, which can be a function or a promise.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
suite.startTest = function(aTestCaseChain) {
|
||||
function setUp() {
|
||||
return ensureTetheringManager()
|
||||
.then(acquirePermission);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
waitFor(finish, function() {
|
||||
return pendingEmulatorShellCount === 0;
|
||||
});
|
||||
}
|
||||
|
||||
return setUp()
|
||||
.then(aTestCaseChain)
|
||||
.then(function onresolve() {
|
||||
tearDown();
|
||||
}, function onreject(aReason) {
|
||||
ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : ''));
|
||||
tearDown();
|
||||
});
|
||||
};
|
||||
|
||||
//---------------------------------------------------
|
||||
// Public test suite functions
|
||||
//---------------------------------------------------
|
||||
suite.ensureWifiEnabled = ensureWifiEnabled;
|
||||
suite.setWifiTetheringEnabled = setWifiTetheringEnabled;
|
||||
suite.getDataApnSettings = getDataApnSettings;
|
||||
suite.setDataApnSettings = setDataApnSettings;
|
||||
suite.setTetheringDunRequired = setTetheringDunRequired;
|
||||
|
||||
|
||||
/**
|
||||
* The common test routine for wifi tethering.
|
||||
*
|
||||
* Set 'ril.data.enabled' to true
|
||||
* before testing and restore it afterward. It will also verify 'ril.data.enabled'
|
||||
* and 'tethering.wifi.enabled' to be false in the beginning. Note that this routine
|
||||
* will NOT change the state of 'tethering.wifi.enabled' so the user should enable
|
||||
* than disable on his/her own. This routine will only check if tethering is turned
|
||||
* off after testing.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
* Reject params: (none)
|
||||
*
|
||||
* @param aTestCaseChain
|
||||
* The test case entry point, which can be a function or a promise.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
suite.startTetheringTest = function(aTestCaseChain) {
|
||||
let oriDataEnabled;
|
||||
function verifyInitialState() {
|
||||
return getSettings(SETTINGS_RIL_DATA_ENABLED)
|
||||
.then(enabled => initTetheringTestEnvironment(enabled));
|
||||
}
|
||||
|
||||
function initTetheringTestEnvironment(aEnabled) {
|
||||
oriDataEnabled = aEnabled;
|
||||
if (aEnabled) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.all([waitForRilDataConnected(true),
|
||||
setSettings1(SETTINGS_RIL_DATA_ENABLED, true)]);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreToInitialState() {
|
||||
return setSettings1(SETTINGS_RIL_DATA_ENABLED, oriDataEnabled);
|
||||
}
|
||||
|
||||
return suite.startTest(function() {
|
||||
return verifyInitialState()
|
||||
.then(aTestCaseChain)
|
||||
.then(restoreToInitialState, function onreject(aReason) {
|
||||
return restoreToInitialState()
|
||||
.then(() => { throw aReason; }); // Re-throw the orignal reject reason.
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return suite;
|
||||
})();
|
@ -1,7 +0,0 @@
|
||||
[DEFAULT]
|
||||
run-if = buildapp == 'b2g'
|
||||
|
||||
[test_wifi_tethering_enabled.js]
|
||||
; The following test must be the last tethering test ran, as it sets the
|
||||
; 'ro.tethering.dun_required' property.
|
||||
[test_wifi_tethering_dun.js]
|
@ -1,37 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
MARIONETTE_TIMEOUT = 60000;
|
||||
MARIONETTE_HEAD_JS = 'head.js';
|
||||
|
||||
gTestSuite.startTest(function() {
|
||||
let origApnSettings;
|
||||
return gTestSuite.getDataApnSettings()
|
||||
.then(value => {
|
||||
origApnSettings = value;
|
||||
})
|
||||
.then(() => {
|
||||
// Set dun apn settings.
|
||||
let apnSettings = [[ { "carrier": "T-Mobile US",
|
||||
"apn": "epc1.tmobile.com",
|
||||
"mmsc": "http://mms.msg.eng.t-mobile.com/mms/wapenc",
|
||||
"types": ["default","supl","mms"] },
|
||||
{ "carrier": "T-Mobile US",
|
||||
"apn": "epc2.tmobile.com",
|
||||
"types": ["dun"] } ]];
|
||||
return gTestSuite.setDataApnSettings(apnSettings);
|
||||
})
|
||||
.then(() => gTestSuite.setTetheringDunRequired())
|
||||
.then(() => gTestSuite.startTetheringTest(function() {
|
||||
return gTestSuite.ensureWifiEnabled(false)
|
||||
.then(() => gTestSuite.setWifiTetheringEnabled(true, true))
|
||||
.then(() => gTestSuite.setWifiTetheringEnabled(false, true));
|
||||
}))
|
||||
// Restore apn settings.
|
||||
.then(() => {
|
||||
if (origApnSettings) {
|
||||
return gTestSuite.setDataApnSettings(origApnSettings);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
MARIONETTE_TIMEOUT = 60000;
|
||||
MARIONETTE_HEAD_JS = 'head.js';
|
||||
|
||||
gTestSuite.startTetheringTest(function() {
|
||||
return gTestSuite.ensureWifiEnabled(false)
|
||||
.then(() => gTestSuite.setWifiTetheringEnabled(true))
|
||||
.then(() => gTestSuite.setWifiTetheringEnabled(false));
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/.
|
||||
*/
|
||||
|
||||
[Constructor(DOMString type, optional DownloadEventInit eventInitDict),
|
||||
Pref="dom.mozDownloads.enabled",
|
||||
ChromeOnly]
|
||||
interface DownloadEvent : Event
|
||||
{
|
||||
readonly attribute DOMDownload? download;
|
||||
};
|
||||
|
||||
dictionary DownloadEventInit : EventInit
|
||||
{
|
||||
DOMDownload? download = null;
|
||||
};
|
@ -1,166 +0,0 @@
|
||||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/.
|
||||
*/
|
||||
|
||||
// Represents the state of a download.
|
||||
// "downloading": The resource is actively transfering.
|
||||
// "stopped" : No network tranfer is happening.
|
||||
// "succeeded" : The resource has been downloaded successfully.
|
||||
// "finalized" : We won't try to download this resource, but the DOM
|
||||
// object is still alive.
|
||||
enum DownloadState {
|
||||
"downloading",
|
||||
"stopped",
|
||||
"succeeded",
|
||||
"finalized"
|
||||
};
|
||||
|
||||
[NoInterfaceObject,
|
||||
NavigatorProperty="mozDownloadManager",
|
||||
JSImplementation="@mozilla.org/downloads/manager;1",
|
||||
Pref="dom.mozDownloads.enabled",
|
||||
ChromeOnly]
|
||||
interface DOMDownloadManager : EventTarget {
|
||||
// This promise returns an array of downloads with all the current
|
||||
// download objects.
|
||||
Promise<sequence<DOMDownload>> getDownloads();
|
||||
|
||||
// Removes one download from the downloads set. Returns a promise resolved
|
||||
// with the finalized download.
|
||||
[UnsafeInPrerendering]
|
||||
Promise<DOMDownload> remove(DOMDownload download);
|
||||
|
||||
// Removes all completed downloads. This kicks off an asynchronous process
|
||||
// that will eventually complete, but will not have completed by the time this
|
||||
// method returns. If you care about the side-effects of this method, know
|
||||
// that each existing download will have its onstatechange method invoked and
|
||||
// will have a new state of "finalized". (After the download is finalized, no
|
||||
// further events will be generated on it.)
|
||||
[UnsafeInPrerendering]
|
||||
void clearAllDone();
|
||||
|
||||
// Add completed downloads from applications that must perform the download
|
||||
// process themselves. For example, email. The method is resolved with a
|
||||
// fully populated DOMDownload instance on success, or rejected in the
|
||||
// event all required options were not provided.
|
||||
//
|
||||
// The adopted download will also be reported via the ondownloadstart event
|
||||
// handler.
|
||||
//
|
||||
// Applications must currently be certified to use this, but it could be
|
||||
// widened at a later time.
|
||||
//
|
||||
// Note that "download" is not actually optional, but WebIDL requires that it
|
||||
// be marked as such because it is not followed by a required argument. The
|
||||
// promise will be rejected if the dictionary is omitted or the specified
|
||||
// file does not exist on disk.
|
||||
Promise<DOMDownload> adoptDownload(optional AdoptDownloadDict download);
|
||||
|
||||
// Fires when a new download starts.
|
||||
attribute EventHandler ondownloadstart;
|
||||
};
|
||||
|
||||
[JSImplementation="@mozilla.org/downloads/download;1",
|
||||
Pref="dom.mozDownloads.enabled",
|
||||
ChromeOnly]
|
||||
interface DOMDownload : EventTarget {
|
||||
// The full size of the resource.
|
||||
readonly attribute long long totalBytes;
|
||||
|
||||
// The number of bytes that we have currently downloaded.
|
||||
readonly attribute long long currentBytes;
|
||||
|
||||
// The url of the resource.
|
||||
readonly attribute DOMString url;
|
||||
|
||||
// The full path in local storage where the file will end up once the download
|
||||
// is complete. This is equivalent to the concatenation of the 'storagePath'
|
||||
// to the 'mountPoint' of the nsIVolume associated with the 'storageName'
|
||||
// (with delimiter).
|
||||
readonly attribute DOMString path;
|
||||
|
||||
// The DeviceStorage volume name on which the file is being downloaded.
|
||||
readonly attribute DOMString storageName;
|
||||
|
||||
// The DeviceStorage path on the volume with 'storageName' of the file being
|
||||
// downloaded.
|
||||
readonly attribute DOMString storagePath;
|
||||
|
||||
// The state of the download. One of: downloading, stopped, succeeded, or
|
||||
// finalized. A finalized download is a download that has been removed /
|
||||
// cleared and is no longer tracked by the download manager and will not
|
||||
// receive any further onstatechange updates.
|
||||
readonly attribute DownloadState state;
|
||||
|
||||
// The mime type for this resource.
|
||||
readonly attribute DOMString contentType;
|
||||
|
||||
// The timestamp this download started.
|
||||
readonly attribute Date startTime;
|
||||
|
||||
// An opaque identifier for this download. All instances of the same
|
||||
// download (eg. in different windows) will have the same id.
|
||||
readonly attribute DOMString id;
|
||||
|
||||
// The manifestURL of the application that added this download. Only used for
|
||||
// downloads added via the adoptDownload API call.
|
||||
readonly attribute DOMString? sourceAppManifestURL;
|
||||
|
||||
// A DOM error object, that will be not null when a download is stopped
|
||||
// because something failed.
|
||||
readonly attribute DOMError? error;
|
||||
|
||||
// Pauses the download.
|
||||
[UnsafeInPrerendering]
|
||||
Promise<DOMDownload> pause();
|
||||
|
||||
// Resumes the download. This resolves only once the download has
|
||||
// succeeded.
|
||||
[UnsafeInPrerendering]
|
||||
Promise<DOMDownload> resume();
|
||||
|
||||
// This event is triggered anytime a property of the object changes:
|
||||
// - when the transfer progresses, updating currentBytes.
|
||||
// - when the state and/or error attributes change.
|
||||
attribute EventHandler onstatechange;
|
||||
};
|
||||
|
||||
// Used to initialize the DOMDownload object for adopted downloads.
|
||||
// fields directly maps to the DOMDownload fields.
|
||||
dictionary AdoptDownloadDict {
|
||||
// The URL of this resource if there is one available. An empty string if
|
||||
// the download is not accessible via URL. An empty string is chosen over
|
||||
// null so that existinc code does not need to null-check but the value is
|
||||
// still falsey. (Note: If you do have a usable URL, you should probably not
|
||||
// be using the adoptDownload API and instead be initiating downloads the
|
||||
// normal way.)
|
||||
DOMString url;
|
||||
|
||||
// The storageName of the DeviceStorage instance the file was saved to.
|
||||
// Required but marked as optional so the bindings don't auto-coerce the value
|
||||
// null to "null".
|
||||
DOMString? storageName;
|
||||
// The path of the file within the DeviceStorage instance named by
|
||||
// 'storageName'. This is used to automatically compute the 'path' of the
|
||||
// download. Note that when DeviceStorage gives you a path to a file, the
|
||||
// first path segment is the name of the specific device storage and you do
|
||||
// *not* want to include this. For example, if DeviceStorage tells you the
|
||||
// file has a path of '/sdcard1/actual/path/file.ext', then the storageName
|
||||
// should be 'sdcard1' and the storagePath should be 'actual/path/file.ext'.
|
||||
//
|
||||
// The existence of the file will be validated will be validated with stat()
|
||||
// and the size the file-system tells us will be what we use.
|
||||
//
|
||||
// Required but marked as optional so the bindings don't auto-coerce the value
|
||||
// null to "null".
|
||||
DOMString? storagePath;
|
||||
|
||||
// The mime type for this resource. Required, but marked as optional because
|
||||
// WebIDL otherwise auto-coerces the value null to "null".
|
||||
DOMString? contentType;
|
||||
|
||||
// The time the download was started. If omitted, the current time is used.
|
||||
Date? startTime;
|
||||
};
|
@ -1,57 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
enum TetheringType {
|
||||
"bluetooth",
|
||||
"usb",
|
||||
"wifi"
|
||||
};
|
||||
|
||||
enum SecurityType {
|
||||
"open",
|
||||
"wpa-psk",
|
||||
"wpa2-psk"
|
||||
};
|
||||
|
||||
dictionary WifiTetheringConfig {
|
||||
DOMString ssid;
|
||||
SecurityType security;
|
||||
DOMString key;
|
||||
};
|
||||
|
||||
dictionary TetheringConfiguration {
|
||||
DOMString ip;
|
||||
DOMString prefix;
|
||||
DOMString startIp;
|
||||
DOMString endIp;
|
||||
DOMString dns1;
|
||||
DOMString dns2;
|
||||
WifiTetheringConfig wifiConfig;
|
||||
};
|
||||
|
||||
[JSImplementation="@mozilla.org/tetheringmanager;1",
|
||||
NavigatorProperty="mozTetheringManager",
|
||||
ChromeOnly]
|
||||
interface MozTetheringManager {
|
||||
/**
|
||||
* Enable/Disable tethering.
|
||||
* @param enabled True to enable tethering, False to disable tethering.
|
||||
* @param type Tethering type to enable/disable.
|
||||
* @param config Configuration should have following fields when enable is True:
|
||||
* - ip ip address.
|
||||
* - prefix mask length.
|
||||
* - startIp start ip address allocated by DHCP server for tethering.
|
||||
* - endIp end ip address allocated by DHCP server for tethering.
|
||||
* - dns1 first DNS server address.
|
||||
* - dns2 second DNS server address.
|
||||
* - wifiConfig wifi tethering configuration
|
||||
* - ssid SSID network name.
|
||||
* - security open, wpa-psk or wpa2-psk.
|
||||
* - key password for wpa-psk or wpa2-psk.
|
||||
* config should not be set when enabled is False.
|
||||
*/
|
||||
Promise<any> setTetheringEnabled(boolean enabled,
|
||||
TetheringType type,
|
||||
optional TetheringConfiguration config);
|
||||
};
|
@ -128,7 +128,6 @@ WEBIDL_FILES = [
|
||||
'DOMStringList.webidl',
|
||||
'DOMStringMap.webidl',
|
||||
'DOMTokenList.webidl',
|
||||
'Downloads.webidl',
|
||||
'DragEvent.webidl',
|
||||
'DynamicsCompressorNode.webidl',
|
||||
'Element.webidl',
|
||||
@ -320,7 +319,6 @@ WEBIDL_FILES = [
|
||||
'MouseScrollEvent.webidl',
|
||||
'MozPowerManager.webidl',
|
||||
'MozSelfSupport.webidl',
|
||||
'MozTetheringManager.webidl',
|
||||
'MozTimeManager.webidl',
|
||||
'MozWakeLock.webidl',
|
||||
'MutationEvent.webidl',
|
||||
@ -687,7 +685,6 @@ GENERATED_EVENTS_WEBIDL_FILES = [
|
||||
'DeviceProximityEvent.webidl',
|
||||
'DeviceStorageAreaChangedEvent.webidl',
|
||||
'DeviceStorageChangeEvent.webidl',
|
||||
'DownloadEvent.webidl',
|
||||
'ErrorEvent.webidl',
|
||||
'FontFaceSetLoadEvent.webidl',
|
||||
'GamepadAxisMoveEvent.webidl',
|
||||
|
@ -141,8 +141,7 @@ struct IMEState;
|
||||
* delegate the actual commands to the editor independent of the XPFE
|
||||
* implementation.
|
||||
*/
|
||||
class EditorBase : public nsIEditor
|
||||
, public nsIEditorIMESupport
|
||||
class EditorBase : public nsIEditorIMESupport
|
||||
, public nsSupportsWeakReference
|
||||
, public nsIPhonetic
|
||||
{
|
||||
|
@ -37,7 +37,6 @@
|
||||
#include "nsIDOMNode.h" // for nsIDOMNode
|
||||
#include "nsIDocument.h" // for nsIDocument
|
||||
#include "nsIEditor.h" // for EditorBase::GetSelection, etc.
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIEditorMailSupport.h" // for nsIEditorMailSupport
|
||||
#include "nsIFocusManager.h" // for nsIFocusManager
|
||||
#include "nsIFormControl.h" // for nsIFormControl, etc.
|
||||
|
@ -103,6 +103,9 @@ public:
|
||||
bool GetReturnInParagraphCreatesNewParagraph();
|
||||
Element* GetSelectionContainer();
|
||||
|
||||
// nsIEditor overrides
|
||||
NS_IMETHOD GetPreferredIMEState(widget::IMEState* aState) override;
|
||||
|
||||
// TextEditor overrides
|
||||
NS_IMETHOD GetIsDocumentEditable(bool* aIsDocumentEditable) override;
|
||||
NS_IMETHOD BeginningOfDocument() override;
|
||||
@ -124,9 +127,6 @@ public:
|
||||
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
|
||||
NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
|
||||
|
||||
// nsIEditorIMESupport overrides
|
||||
NS_IMETHOD GetPreferredIMEState(widget::IMEState* aState) override;
|
||||
|
||||
// nsIHTMLEditor methods
|
||||
NS_DECL_NSIHTMLEDITOR
|
||||
|
||||
|
@ -51,7 +51,6 @@
|
||||
#include "nsIDOMNode.h"
|
||||
#include "nsIDocument.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIEditorMailSupport.h"
|
||||
#include "nsIEditRules.h"
|
||||
#include "nsIFile.h"
|
||||
|
@ -27,7 +27,6 @@
|
||||
#include "nsIContentIterator.h"
|
||||
#include "nsIDOMElement.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIEditRules.h"
|
||||
#include "nsNameSpaceManager.h"
|
||||
#include "nsINode.h"
|
||||
|
@ -40,7 +40,6 @@
|
||||
#include "nsIDOMNode.h"
|
||||
#include "nsIDOMNodeList.h"
|
||||
#include "nsIDocumentEncoder.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIEditRules.h"
|
||||
#include "nsINode.h"
|
||||
#include "nsIPresShell.h"
|
||||
|
@ -28,7 +28,6 @@
|
||||
#include "nsIDragService.h"
|
||||
#include "nsIDragSession.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIDocShell.h"
|
||||
#include "nsIDocShellTreeItem.h"
|
||||
#include "nsIPrincipal.h"
|
||||
|
@ -34,14 +34,12 @@ SimpleTest.waitForFocus(()=>{
|
||||
return isNSEditableElement() ? SpecialPowers.wrap(aEditor)
|
||||
.QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement)
|
||||
.editor
|
||||
.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport)
|
||||
.composing :
|
||||
SpecialPowers.wrap(window)
|
||||
.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
|
||||
.getInterface(SpecialPowers.Ci.nsIWebNavigation)
|
||||
.QueryInterface(SpecialPowers.Ci.nsIDocShell)
|
||||
.editor
|
||||
.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport)
|
||||
.composing;
|
||||
}
|
||||
function clear() {
|
||||
|
@ -20,17 +20,15 @@ SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var gInputElement = document.getElementById("input");
|
||||
|
||||
function getEditorIMESupport(aInputElement)
|
||||
function getEditor(aInputElement)
|
||||
{
|
||||
var editableElement = SpecialPowers.wrap(aInputElement).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
|
||||
ok(editableElement, "The input element doesn't have nsIDOMNSEditableElement interface");
|
||||
ok(editableElement.editor, "There is no editor for the input element");
|
||||
var editorIMESupport = SpecialPowers.wrap(editableElement).editor.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
|
||||
ok(editorIMESupport, "The input element doesn't have nsIEditorIMESupport interface");
|
||||
return editorIMESupport;
|
||||
return editableElement.editor;
|
||||
}
|
||||
|
||||
var gEditorIMESupport;
|
||||
var gEditor;
|
||||
|
||||
function testNotGenerateCompositionByCreatedEvents(aEventInterface)
|
||||
{
|
||||
@ -41,7 +39,7 @@ function testNotGenerateCompositionByCreatedEvents(aEventInterface)
|
||||
compositionEvent.initMouseEvent("compositionstart", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
}
|
||||
gInputElement.dispatchEvent(compositionEvent);
|
||||
ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")");
|
||||
ok(!gEditor.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")");
|
||||
|
||||
compositionEvent = document.createEvent(aEventInterface);
|
||||
if (compositionEvent.initCompositionEvent) {
|
||||
@ -50,7 +48,7 @@ function testNotGenerateCompositionByCreatedEvents(aEventInterface)
|
||||
compositionEvent.initMouseEvent("compositionupdate", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
}
|
||||
gInputElement.dispatchEvent(compositionEvent);
|
||||
ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")");
|
||||
ok(!gEditor.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")");
|
||||
is(gInputElement.value, "", "Input element shouldn't be modified with a created compositionupdate event (" + aEventInterface + ")");
|
||||
|
||||
compositionEvent = document.createEvent(aEventInterface);
|
||||
@ -60,14 +58,14 @@ function testNotGenerateCompositionByCreatedEvents(aEventInterface)
|
||||
compositionEvent.initMouseEvent("compositionend", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
}
|
||||
gInputElement.dispatchEvent(compositionEvent);
|
||||
ok(!gEditorIMESupport.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
|
||||
ok(!gEditor.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
|
||||
is(gInputElement.value, "", "Input element shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
|
||||
}
|
||||
|
||||
function doTests()
|
||||
{
|
||||
gInputElement.focus();
|
||||
gEditorIMESupport = getEditorIMESupport(gInputElement);
|
||||
gEditor = getEditor(gInputElement);
|
||||
|
||||
testNotGenerateCompositionByCreatedEvents("CompositionEvent");
|
||||
testNotGenerateCompositionByCreatedEvents("MouseEvent");
|
||||
|
@ -21,6 +21,16 @@ interface nsIEditActionListener;
|
||||
interface nsIInlineSpellChecker;
|
||||
interface nsITransferable;
|
||||
|
||||
%{C++
|
||||
namespace mozilla {
|
||||
namespace widget {
|
||||
struct IMEState;
|
||||
} // namespace widget
|
||||
} // namespace mozilla
|
||||
%}
|
||||
|
||||
native IMEState(mozilla::widget::IMEState);
|
||||
|
||||
[scriptable, uuid(094be624-f0bf-400f-89e2-6a84baab9474)]
|
||||
interface nsIEditor : nsISupports
|
||||
{
|
||||
@ -564,4 +574,19 @@ interface nsIEditor : nsISupports
|
||||
* or nsIEditorObserver::CancelEditAction(). Otherwise, false.
|
||||
*/
|
||||
[noscript] readonly attribute boolean isInEditAction;
|
||||
|
||||
/**
|
||||
* forceCompositionEnd() force the composition end
|
||||
*/
|
||||
void forceCompositionEnd();
|
||||
|
||||
/**
|
||||
* Get preferred IME status of current widget.
|
||||
*/
|
||||
[noscript] IMEState getPreferredIMEState();
|
||||
|
||||
/**
|
||||
* whether this editor has active IME transaction
|
||||
*/
|
||||
readonly attribute boolean composing;
|
||||
};
|
||||
|
@ -3,38 +3,9 @@
|
||||
* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
#include "domstubs.idl"
|
||||
|
||||
%{C++
|
||||
namespace mozilla {
|
||||
namespace widget {
|
||||
struct IMEState;
|
||||
} // namespace widget
|
||||
} // namespace mozilla
|
||||
%}
|
||||
|
||||
native IMEState(mozilla::widget::IMEState);
|
||||
#include "nsIEditor.idl"
|
||||
|
||||
[scriptable, uuid(0ba7f490-afb8-46dd-87fc-bc6137fbc899)]
|
||||
|
||||
interface nsIEditorIMESupport : nsISupports
|
||||
interface nsIEditorIMESupport : nsIEditor
|
||||
{
|
||||
/**
|
||||
* forceCompositionEnd() force the composition end
|
||||
*/
|
||||
|
||||
void forceCompositionEnd();
|
||||
|
||||
/**
|
||||
* Get preferred IME status of current widget.
|
||||
*/
|
||||
|
||||
[noscript] IMEState getPreferredIMEState();
|
||||
|
||||
/**
|
||||
* whether this editor has active IME transaction
|
||||
*/
|
||||
readonly attribute boolean composing;
|
||||
};
|
||||
|
||||
|
@ -736,7 +736,7 @@ struct JSClass {
|
||||
// application.
|
||||
#define JSCLASS_GLOBAL_APPLICATION_SLOTS 5
|
||||
#define JSCLASS_GLOBAL_SLOT_COUNT \
|
||||
(JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 39)
|
||||
(JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 40)
|
||||
#define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \
|
||||
(JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n)))
|
||||
#define JSCLASS_GLOBAL_FLAGS \
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "builtin/Intl.h"
|
||||
|
||||
#include "mozilla/Casting.h"
|
||||
#include "mozilla/PodOperations.h"
|
||||
#include "mozilla/Range.h"
|
||||
|
||||
@ -25,6 +26,7 @@
|
||||
#include "builtin/IntlTimeZoneData.h"
|
||||
#include "ds/Sort.h"
|
||||
#if ENABLE_INTL_API
|
||||
#include "unicode/plurrule.h"
|
||||
#include "unicode/ucal.h"
|
||||
#include "unicode/ucol.h"
|
||||
#include "unicode/udat.h"
|
||||
@ -32,6 +34,7 @@
|
||||
#include "unicode/uenum.h"
|
||||
#include "unicode/unum.h"
|
||||
#include "unicode/unumsys.h"
|
||||
#include "unicode/upluralrules.h"
|
||||
#include "unicode/ustring.h"
|
||||
#endif
|
||||
#include "vm/DateTime.h"
|
||||
@ -47,6 +50,7 @@
|
||||
|
||||
using namespace js;
|
||||
|
||||
using mozilla::AssertedCast;
|
||||
using mozilla::IsFinite;
|
||||
using mozilla::IsNaN;
|
||||
using mozilla::IsNegativeZero;
|
||||
@ -79,15 +83,46 @@ using mozilla::RangedPtr;
|
||||
|
||||
namespace {
|
||||
|
||||
typedef bool UBool;
|
||||
typedef char16_t UChar;
|
||||
typedef double UDate;
|
||||
|
||||
enum UErrorCode {
|
||||
U_ZERO_ERROR,
|
||||
U_BUFFER_OVERFLOW_ERROR,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace icu {
|
||||
|
||||
class StringEnumeration {
|
||||
public:
|
||||
explicit StringEnumeration();
|
||||
};
|
||||
|
||||
StringEnumeration::StringEnumeration()
|
||||
{
|
||||
MOZ_CRASH("StringEnumeration::StringEnumeration: Intl API disabled");
|
||||
}
|
||||
|
||||
class PluralRules {
|
||||
public:
|
||||
|
||||
StringEnumeration* getKeywords(UErrorCode& status) const;
|
||||
|
||||
};
|
||||
|
||||
StringEnumeration*
|
||||
PluralRules::getKeywords(UErrorCode& status) const
|
||||
{
|
||||
MOZ_CRASH("PluralRules::getKeywords: Intl API disabled");
|
||||
}
|
||||
|
||||
} // icu namespace
|
||||
|
||||
namespace {
|
||||
|
||||
typedef bool UBool;
|
||||
typedef char16_t UChar;
|
||||
typedef double UDate;
|
||||
|
||||
inline UBool
|
||||
U_FAILURE(UErrorCode code)
|
||||
{
|
||||
@ -118,6 +153,32 @@ UCharToChar16(const UChar* chars)
|
||||
MOZ_CRASH("UCharToChar16: Intl API disabled");
|
||||
}
|
||||
|
||||
const char*
|
||||
uloc_getAvailable(int32_t n)
|
||||
{
|
||||
MOZ_CRASH("uloc_getAvailable: Intl API disabled");
|
||||
}
|
||||
|
||||
int32_t
|
||||
uloc_countAvailable()
|
||||
{
|
||||
MOZ_CRASH("uloc_countAvailable: Intl API disabled");
|
||||
}
|
||||
|
||||
struct UFormattable;
|
||||
|
||||
void
|
||||
ufmt_close(UFormattable* fmt)
|
||||
{
|
||||
MOZ_CRASH("ufmt_close: Intl API disabled");
|
||||
}
|
||||
|
||||
double
|
||||
ufmt_getDouble(UFormattable* fmt, UErrorCode *status)
|
||||
{
|
||||
MOZ_CRASH("ufmt_getDouble: Intl API disabled");
|
||||
}
|
||||
|
||||
struct UEnumeration;
|
||||
|
||||
int32_t
|
||||
@ -138,6 +199,12 @@ uenum_close(UEnumeration* en)
|
||||
MOZ_CRASH("uenum_close: Intl API disabled");
|
||||
}
|
||||
|
||||
UEnumeration*
|
||||
uenum_openFromStringEnumeration(icu::StringEnumeration* adopted, UErrorCode* ec)
|
||||
{
|
||||
MOZ_CRASH("uenum_openFromStringEnumeration: Intl API disabled");
|
||||
}
|
||||
|
||||
struct UCollator;
|
||||
|
||||
enum UColAttribute {
|
||||
@ -317,6 +384,17 @@ unum_setTextAttribute(UNumberFormat* fmt, UNumberFormatTextAttribute tag, const
|
||||
MOZ_CRASH("unum_setTextAttribute: Intl API disabled");
|
||||
}
|
||||
|
||||
UFormattable*
|
||||
unum_parseToUFormattable(const UNumberFormat* fmt,
|
||||
UFormattable *result,
|
||||
const UChar* text,
|
||||
int32_t textLength,
|
||||
int32_t* parsePos, /* 0 = start */
|
||||
UErrorCode* status)
|
||||
{
|
||||
MOZ_CRASH("unum_parseToUFormattable: Intl API disabled");
|
||||
}
|
||||
|
||||
typedef void* UNumberingSystem;
|
||||
|
||||
UNumberingSystem*
|
||||
@ -681,6 +759,34 @@ udat_getSymbols(const UDateFormat *fmt, UDateFormatSymbolType type, int32_t symb
|
||||
MOZ_CRASH("udat_getSymbols: Intl API disabled");
|
||||
}
|
||||
|
||||
typedef void* UPluralRules;
|
||||
|
||||
enum UPluralType {
|
||||
UPLURAL_TYPE_CARDINAL,
|
||||
UPLURAL_TYPE_ORDINAL
|
||||
};
|
||||
|
||||
void
|
||||
uplrules_close(UPluralRules *uplrules)
|
||||
{
|
||||
MOZ_CRASH("uplrules_close: Intl API disabled");
|
||||
}
|
||||
|
||||
UPluralRules*
|
||||
uplrules_openForType(const char *locale, UPluralType type, UErrorCode *status)
|
||||
{
|
||||
MOZ_CRASH("uplrules_openForType: Intl API disabled");
|
||||
}
|
||||
|
||||
int32_t
|
||||
uplrules_select(const UPluralRules *uplrules,
|
||||
double number,
|
||||
UChar *keyword, int32_t capacity,
|
||||
UErrorCode *status)
|
||||
{
|
||||
MOZ_CRASH("uplrules_select: Intl API disabled");
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
#endif
|
||||
@ -1612,6 +1718,100 @@ js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* This creates new UNumberFormat with calculated digit formatting
|
||||
* properties for PluralRules.
|
||||
*
|
||||
* This is similar to NewUNumberFormat but doesn't allow for currency or
|
||||
* percent types.
|
||||
*
|
||||
*/
|
||||
static UNumberFormat*
|
||||
NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules)
|
||||
{
|
||||
RootedObject internals(cx, GetInternals(cx, pluralRules));
|
||||
if (!internals)
|
||||
return nullptr;
|
||||
|
||||
RootedValue value(cx);
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().locale, &value))
|
||||
return nullptr;
|
||||
JSAutoByteString locale(cx, value.toString());
|
||||
if (!locale)
|
||||
return nullptr;
|
||||
|
||||
uint32_t uMinimumIntegerDigits = 1;
|
||||
uint32_t uMinimumFractionDigits = 0;
|
||||
uint32_t uMaximumFractionDigits = 3;
|
||||
int32_t uMinimumSignificantDigits = -1;
|
||||
int32_t uMaximumSignificantDigits = -1;
|
||||
|
||||
RootedId id(cx, NameToId(cx->names().minimumSignificantDigits));
|
||||
bool hasP;
|
||||
if (!HasProperty(cx, internals, id, &hasP))
|
||||
return nullptr;
|
||||
if (hasP) {
|
||||
if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumSignificantDigits = value.toInt32();
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMaximumSignificantDigits = value.toInt32();
|
||||
} else {
|
||||
if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMaximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
}
|
||||
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, icuLocale(locale.ptr()), nullptr, &status);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return nullptr;
|
||||
}
|
||||
ScopedICUObject<UNumberFormat, unum_close> toClose(nf);
|
||||
|
||||
if (uMinimumSignificantDigits != -1) {
|
||||
unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true);
|
||||
unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits);
|
||||
unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits);
|
||||
} else {
|
||||
unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits);
|
||||
unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits);
|
||||
unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits);
|
||||
}
|
||||
|
||||
return toClose.forget();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new UNumberFormat with the locale and number formatting options
|
||||
* of the given NumberFormat.
|
||||
@ -1695,32 +1895,32 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumSignificantDigits = int32_t(value.toNumber());
|
||||
uMinimumSignificantDigits = value.toInt32();
|
||||
if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMaximumSignificantDigits = int32_t(value.toNumber());
|
||||
uMaximumSignificantDigits = value.toInt32();
|
||||
} else {
|
||||
if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumIntegerDigits = int32_t(value.toNumber());
|
||||
uMinimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMinimumFractionDigits = int32_t(value.toNumber());
|
||||
uMinimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits,
|
||||
&value))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
uMaximumFractionDigits = int32_t(value.toNumber());
|
||||
uMaximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||
}
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value))
|
||||
@ -3427,6 +3627,381 @@ js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**************** PluralRules *****************/
|
||||
|
||||
static void pluralRules_finalize(FreeOp* fop, JSObject* obj);
|
||||
|
||||
static const uint32_t UPLURAL_RULES_SLOT = 0;
|
||||
static const uint32_t PLURAL_RULES_SLOTS_COUNT = 1;
|
||||
|
||||
static const ClassOps PluralRulesClassOps = {
|
||||
nullptr, /* addProperty */
|
||||
nullptr, /* delProperty */
|
||||
nullptr, /* getProperty */
|
||||
nullptr, /* setProperty */
|
||||
nullptr, /* enumerate */
|
||||
nullptr, /* resolve */
|
||||
nullptr, /* mayResolve */
|
||||
pluralRules_finalize
|
||||
};
|
||||
|
||||
static const Class PluralRulesClass = {
|
||||
js_Object_str,
|
||||
JSCLASS_HAS_RESERVED_SLOTS(PLURAL_RULES_SLOTS_COUNT) |
|
||||
JSCLASS_FOREGROUND_FINALIZE,
|
||||
&PluralRulesClassOps
|
||||
};
|
||||
|
||||
#if JS_HAS_TOSOURCE
|
||||
static bool
|
||||
pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
args.rval().setString(cx->names().PluralRules);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
static const JSFunctionSpec pluralRules_static_methods[] = {
|
||||
JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_PluralRules_supportedLocalesOf", 1, 0),
|
||||
JS_FS_END
|
||||
};
|
||||
|
||||
static const JSFunctionSpec pluralRules_methods[] = {
|
||||
JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, 0),
|
||||
JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0),
|
||||
#if JS_HAS_TOSOURCE
|
||||
JS_FN(js_toSource_str, pluralRules_toSource, 0, 0),
|
||||
#endif
|
||||
JS_FS_END
|
||||
};
|
||||
|
||||
/**
|
||||
* PluralRules constructor.
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.1
|
||||
*/
|
||||
static bool
|
||||
PluralRules(JSContext* cx, const CallArgs& args, bool construct)
|
||||
{
|
||||
RootedObject obj(cx);
|
||||
|
||||
if (!construct) {
|
||||
JSObject* intl = cx->global()->getOrCreateIntlObject(cx);
|
||||
if (!intl)
|
||||
return false;
|
||||
RootedValue self(cx, args.thisv());
|
||||
if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) {
|
||||
obj = ToObject(cx, self);
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
bool extensible;
|
||||
if (!IsExtensible(cx, obj, &extensible))
|
||||
return false;
|
||||
if (!extensible)
|
||||
return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE);
|
||||
} else {
|
||||
construct = true;
|
||||
}
|
||||
}
|
||||
if (construct) {
|
||||
RootedObject proto(cx, cx->global()->getOrCreatePluralRulesPrototype(cx));
|
||||
if (!proto)
|
||||
return false;
|
||||
obj = NewObjectWithGivenProto(cx, &PluralRulesClass, proto);
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
obj->as<NativeObject>().setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr));
|
||||
}
|
||||
|
||||
RootedValue locales(cx, args.get(0));
|
||||
RootedValue options(cx, args.get(1));
|
||||
|
||||
if (!IntlInitialize(cx, obj, cx->names().InitializePluralRules, locales, options))
|
||||
return false;
|
||||
|
||||
args.rval().setObject(*obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
PluralRules(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
return PluralRules(cx, args, args.isConstructing());
|
||||
}
|
||||
|
||||
bool
|
||||
js::intl_PluralRules(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 2);
|
||||
return PluralRules(cx, args, true);
|
||||
}
|
||||
|
||||
static void
|
||||
pluralRules_finalize(FreeOp* fop, JSObject* obj)
|
||||
{
|
||||
MOZ_ASSERT(fop->onMainThread());
|
||||
|
||||
// This is-undefined check shouldn't be necessary, but for internal
|
||||
// brokenness in object allocation code. For the moment, hack around it by
|
||||
// explicitly guarding against the possibility of the reserved slot not
|
||||
// containing a private. See bug 949220.
|
||||
const Value& slot = obj->as<NativeObject>().getReservedSlot(UPLURAL_RULES_SLOT);
|
||||
if (!slot.isUndefined()) {
|
||||
if (UPluralRules* pr = static_cast<UPluralRules*>(slot.toPrivate()))
|
||||
uplrules_close(pr);
|
||||
}
|
||||
}
|
||||
|
||||
static JSObject*
|
||||
CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global)
|
||||
{
|
||||
RootedFunction ctor(cx);
|
||||
ctor = global->createConstructor(cx, &PluralRules, cx->names().PluralRules, 0);
|
||||
if (!ctor)
|
||||
return nullptr;
|
||||
|
||||
RootedNativeObject proto(cx, global->createBlankPrototype(cx, &PluralRulesClass));
|
||||
if (!proto)
|
||||
return nullptr;
|
||||
proto->setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr));
|
||||
|
||||
if (!LinkConstructorAndPrototype(cx, ctor, proto))
|
||||
return nullptr;
|
||||
|
||||
if (!JS_DefineFunctions(cx, ctor, pluralRules_static_methods))
|
||||
return nullptr;
|
||||
|
||||
if (!JS_DefineFunctions(cx, proto, pluralRules_methods))
|
||||
return nullptr;
|
||||
|
||||
RootedValue options(cx);
|
||||
if (!CreateDefaultOptions(cx, &options))
|
||||
return nullptr;
|
||||
|
||||
if (!IntlInitialize(cx, proto, cx->names().InitializePluralRules, UndefinedHandleValue,
|
||||
options))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RootedValue ctorValue(cx, ObjectValue(*ctor));
|
||||
if (!DefineProperty(cx, Intl, cx->names().PluralRules, ctorValue, nullptr, nullptr, 0))
|
||||
return nullptr;
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
bool
|
||||
js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 0);
|
||||
|
||||
RootedValue result(cx);
|
||||
// We're going to use ULocale availableLocales as per ICU recommendation:
|
||||
// https://ssl.icu-project.org/trac/ticket/12756
|
||||
if (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result))
|
||||
return false;
|
||||
args.rval().set(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
|
||||
RootedObject pluralRules(cx, &args[0].toObject());
|
||||
|
||||
UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules);
|
||||
if (!nf)
|
||||
return false;
|
||||
|
||||
ScopedICUObject<UNumberFormat, unum_close> closeNumberFormat(nf);
|
||||
|
||||
RootedObject internals(cx, GetInternals(cx, pluralRules));
|
||||
if (!internals)
|
||||
return false;
|
||||
|
||||
RootedValue value(cx);
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().locale, &value))
|
||||
return false;
|
||||
JSAutoByteString locale(cx, value.toString());
|
||||
if (!locale)
|
||||
return false;
|
||||
|
||||
if (!GetProperty(cx, internals, internals, cx->names().type, &value))
|
||||
return false;
|
||||
JSAutoByteString type(cx, value.toString());
|
||||
if (!type)
|
||||
return false;
|
||||
|
||||
double x = args[1].toNumber();
|
||||
|
||||
// We need a NumberFormat in order to format the number
|
||||
// using the number formatting options (minimum/maximum*Digits)
|
||||
// before we push the result to PluralRules
|
||||
//
|
||||
// This should be fixed in ICU 59 and we'll be able to switch to that
|
||||
// API: http://bugs.icu-project.org/trac/ticket/12763
|
||||
//
|
||||
RootedValue fmtNumValue(cx);
|
||||
if (!intl_FormatNumber(cx, nf, x, &fmtNumValue))
|
||||
return false;
|
||||
RootedString fmtNumValueString(cx, fmtNumValue.toString());
|
||||
AutoStableStringChars stableChars(cx);
|
||||
if (!stableChars.initTwoByte(cx, fmtNumValueString))
|
||||
return false;
|
||||
|
||||
const UChar* uFmtNumValue = Char16ToUChar(stableChars.twoByteRange().begin().get());
|
||||
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
UFormattable* fmt = unum_parseToUFormattable(nf, nullptr, uFmtNumValue,
|
||||
stableChars.twoByteRange().length(), 0, &status);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedICUObject<UFormattable, ufmt_close> closeUFormattable(fmt);
|
||||
|
||||
double y = ufmt_getDouble(fmt, &status);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
UPluralType category;
|
||||
|
||||
if (equal(type, "cardinal")) {
|
||||
category = UPLURAL_TYPE_CARDINAL;
|
||||
} else {
|
||||
MOZ_ASSERT(equal(type, "ordinal"));
|
||||
category = UPLURAL_TYPE_ORDINAL;
|
||||
}
|
||||
|
||||
UPluralRules* pr = uplrules_openForType(icuLocale(locale.ptr()), category, &status);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedICUObject<UPluralRules, uplrules_close> closePluralRules(pr);
|
||||
|
||||
Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
|
||||
if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE))
|
||||
return false;
|
||||
|
||||
int size = uplrules_select(pr, y, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, &status);
|
||||
if (status == U_BUFFER_OVERFLOW_ERROR) {
|
||||
if (!chars.resize(size))
|
||||
return false;
|
||||
status = U_ZERO_ERROR;
|
||||
uplrules_select(pr, y, Char16ToUChar(chars.begin()), size, &status);
|
||||
}
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), size);
|
||||
if (!str)
|
||||
return false;
|
||||
|
||||
args.rval().setString(str);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 2);
|
||||
|
||||
JSAutoByteString locale(cx, args[0].toString());
|
||||
if (!locale)
|
||||
return false;
|
||||
|
||||
JSAutoByteString type(cx, args[1].toString());
|
||||
if (!type)
|
||||
return false;
|
||||
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
UPluralType category;
|
||||
|
||||
if (equal(type, "cardinal")) {
|
||||
category = UPLURAL_TYPE_CARDINAL;
|
||||
} else {
|
||||
MOZ_ASSERT(equal(type, "ordinal"));
|
||||
category = UPLURAL_TYPE_ORDINAL;
|
||||
}
|
||||
|
||||
UPluralRules* pr = uplrules_openForType(
|
||||
icuLocale(locale.ptr()),
|
||||
category,
|
||||
&status
|
||||
);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedICUObject<UPluralRules, uplrules_close> closePluralRules(pr);
|
||||
|
||||
// We should get a C API for that in ICU 59 and switch to it
|
||||
// https://ssl.icu-project.org/trac/ticket/12772
|
||||
//
|
||||
icu::StringEnumeration* kwenum =
|
||||
reinterpret_cast<icu::PluralRules*>(pr)->getKeywords(status);
|
||||
UEnumeration* ue = uenum_openFromStringEnumeration(kwenum, &status);
|
||||
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedICUObject<UEnumeration, uenum_close> closeEnum(ue);
|
||||
|
||||
RootedObject res(cx, NewDenseEmptyArray(cx));
|
||||
if (!res)
|
||||
return false;
|
||||
|
||||
RootedValue element(cx);
|
||||
uint32_t i = 0;
|
||||
int32_t catSize;
|
||||
const char* cat;
|
||||
|
||||
do {
|
||||
cat = uenum_next(ue, &catSize, &status);
|
||||
if (U_FAILURE(status)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cat)
|
||||
break;
|
||||
|
||||
JSString* str = NewStringCopyN<CanGC>(cx, cat, catSize);
|
||||
if (!str)
|
||||
return false;
|
||||
|
||||
element.setString(str);
|
||||
if (!DefineElement(cx, res, i, element))
|
||||
return false;
|
||||
i++;
|
||||
} while (true);
|
||||
|
||||
args.rval().setObject(*res);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
@ -3916,6 +4491,9 @@ GlobalObject::initIntlObject(JSContext* cx, Handle<GlobalObject*> global)
|
||||
RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global));
|
||||
if (!numberFormatProto)
|
||||
return false;
|
||||
RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global));
|
||||
if (!pluralRulesProto)
|
||||
return false;
|
||||
|
||||
// The |Intl| object is fully set up now, so define the global property.
|
||||
RootedValue intlValue(cx, ObjectValue(*intl));
|
||||
@ -3937,6 +4515,7 @@ GlobalObject::initIntlObject(JSContext* cx, Handle<GlobalObject*> global)
|
||||
global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto));
|
||||
global->setReservedSlot(DATE_TIME_FORMAT_PROTO, ObjectValue(*dateTimeFormatProto));
|
||||
global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto));
|
||||
global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto));
|
||||
|
||||
// Also cache |Intl| to implement spec language that conditions behavior
|
||||
// based on values being equal to "the standard built-in |Intl| object".
|
||||
|
@ -363,6 +363,54 @@ intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp);
|
||||
extern MOZ_MUST_USE bool
|
||||
intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp);
|
||||
|
||||
/******************** PluralRules ********************/
|
||||
|
||||
/**
|
||||
* Returns a new PluralRules instance.
|
||||
* Self-hosted code cannot cache this constructor (as it does for others in
|
||||
* Utilities.js) because it is initialized after self-hosted code is compiled.
|
||||
*
|
||||
* Usage: pluralRules = intl_PluralRules(locales, options)
|
||||
*/
|
||||
extern MOZ_MUST_USE bool
|
||||
intl_PluralRules(JSContext* cx, unsigned argc, Value* vp);
|
||||
|
||||
/**
|
||||
* Returns an object indicating the supported locales for plural rules
|
||||
* by having a true-valued property for each such locale with the
|
||||
* canonicalized language tag as the property name. The object has no
|
||||
* prototype.
|
||||
*
|
||||
* Usage: availableLocales = intl_PluralRules_availableLocales()
|
||||
*/
|
||||
extern MOZ_MUST_USE bool
|
||||
intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp);
|
||||
|
||||
/**
|
||||
* Returns a plural rule for the number x according to the effective
|
||||
* locale and the formatting options of the given PluralRules.
|
||||
*
|
||||
* A plural rule is a grammatical category that expresses count distinctions
|
||||
* (such as "one", "two", "few" etc.).
|
||||
*
|
||||
* Usage: rule = intl_SelectPluralRule(pluralRules, x)
|
||||
*/
|
||||
extern MOZ_MUST_USE bool
|
||||
intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp);
|
||||
|
||||
/**
|
||||
* Returns an array of plural rules categories for a given
|
||||
* locale and type.
|
||||
*
|
||||
* Usage: categories = intl_GetPluralCategories(locale, type)
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* intl_getPluralCategories('pl', 'cardinal'); // ['one', 'few', 'many', 'other']
|
||||
*/
|
||||
extern MOZ_MUST_USE bool
|
||||
intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp);
|
||||
|
||||
/**
|
||||
* Returns a plain object with calendar information for a single valid locale
|
||||
* (callers must perform this validation). The object will have these
|
||||
|
@ -21,6 +21,9 @@
|
||||
intl_availableCalendars: false,
|
||||
intl_patternForSkeleton: false,
|
||||
intl_FormatDateTime: false,
|
||||
intl_SelectPluralRule: false,
|
||||
intl_GetPluralCategories: false,
|
||||
intl_GetCalendarInfo: false,
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -839,6 +842,7 @@ function BestAvailableLocaleIgnoringDefault(availableLocales, locale) {
|
||||
return BestAvailableLocaleHelper(availableLocales, locale, false);
|
||||
}
|
||||
|
||||
var noRelevantExtensionKeys = [];
|
||||
|
||||
/**
|
||||
* Compares a BCP 47 language priority list against the set of locales in
|
||||
@ -1252,7 +1256,9 @@ function initializeIntlObject(obj) {
|
||||
function setLazyData(internals, type, lazyData)
|
||||
{
|
||||
assert(internals.type === "partial", "can't set lazy data for anything but a newborn");
|
||||
assert(type === "Collator" || type === "DateTimeFormat" || type == "NumberFormat", "bad type");
|
||||
assert(type === "Collator" || type === "DateTimeFormat" ||
|
||||
type == "NumberFormat" || type === "PluralRules",
|
||||
"bad type");
|
||||
assert(IsObject(lazyData), "non-object lazy data");
|
||||
|
||||
// Set in reverse order so that the .type change is a barrier.
|
||||
@ -1302,7 +1308,9 @@ function isInitializedIntlObject(obj) {
|
||||
if (IsObject(internals)) {
|
||||
assert(callFunction(std_Object_hasOwnProperty, internals, "type"), "missing type");
|
||||
var type = internals.type;
|
||||
assert(type === "partial" || type === "Collator" || type === "DateTimeFormat" || type === "NumberFormat", "unexpected type");
|
||||
assert(type === "partial" || type === "Collator" ||
|
||||
type === "DateTimeFormat" || type === "NumberFormat" || type === "PluralRules",
|
||||
"unexpected type");
|
||||
assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData");
|
||||
assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps");
|
||||
} else {
|
||||
@ -1359,6 +1367,8 @@ function getInternals(obj)
|
||||
internalProps = resolveCollatorInternals(lazyData)
|
||||
else if (type === "DateTimeFormat")
|
||||
internalProps = resolveDateTimeFormatInternals(lazyData)
|
||||
else if (type === "PluralRules")
|
||||
internalProps = resolvePluralRulesInternals(lazyData)
|
||||
else
|
||||
internalProps = resolveNumberFormatInternals(lazyData);
|
||||
setInternalProperties(internals, internalProps);
|
||||
@ -1758,45 +1768,37 @@ function resolveNumberFormatInternals(lazyNumberFormatData) {
|
||||
// Step 6.
|
||||
var opt = lazyNumberFormatData.opt;
|
||||
|
||||
// Compute effective locale.
|
||||
// Step 9.
|
||||
var NumberFormat = numberFormatInternalProperties;
|
||||
|
||||
// Step 10.
|
||||
// Step 9.
|
||||
var localeData = NumberFormat.localeData;
|
||||
|
||||
// Step 11.
|
||||
// Step 10.
|
||||
var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat),
|
||||
lazyNumberFormatData.requestedLocales,
|
||||
lazyNumberFormatData.opt,
|
||||
NumberFormat.relevantExtensionKeys,
|
||||
localeData);
|
||||
|
||||
// Steps 12-13. (Step 14 is not relevant to our implementation.)
|
||||
// Steps 11-12. (Step 13 is not relevant to our implementation.)
|
||||
internalProps.locale = r.locale;
|
||||
internalProps.numberingSystem = r.nu;
|
||||
|
||||
// Compute formatting options.
|
||||
// Step 16.
|
||||
// Step 15.
|
||||
var s = lazyNumberFormatData.style;
|
||||
internalProps.style = s;
|
||||
|
||||
// Steps 20, 22.
|
||||
// Steps 19, 21.
|
||||
if (s === "currency") {
|
||||
internalProps.currency = lazyNumberFormatData.currency;
|
||||
internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay;
|
||||
}
|
||||
|
||||
// Step 24.
|
||||
internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits;
|
||||
|
||||
// Steps 27.
|
||||
internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits;
|
||||
|
||||
// Step 30.
|
||||
internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits;
|
||||
|
||||
// Step 33.
|
||||
if ("minimumSignificantDigits" in lazyNumberFormatData) {
|
||||
// Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the
|
||||
// actual presence (versus undefined-ness) of these properties.
|
||||
@ -1805,10 +1807,10 @@ function resolveNumberFormatInternals(lazyNumberFormatData) {
|
||||
internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits;
|
||||
}
|
||||
|
||||
// Step 35.
|
||||
// Step 27.
|
||||
internalProps.useGrouping = lazyNumberFormatData.useGrouping;
|
||||
|
||||
// Step 42.
|
||||
// Step 34.
|
||||
internalProps.boundFormat = undefined;
|
||||
|
||||
// The caller is responsible for associating |internalProps| with the right
|
||||
@ -1836,6 +1838,42 @@ function getNumberFormatInternals(obj, methodName) {
|
||||
return internalProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies digit options used for number formatting onto the intl object.
|
||||
*
|
||||
* Spec: ECMAScript Internationalization API Specification, 11.1.1.
|
||||
*/
|
||||
function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault) {
|
||||
// We skip Step 1 because we set the properties on a lazyData object.
|
||||
|
||||
// Step 2-3.
|
||||
assert(IsObject(options), "SetNumberFormatDigitOptions");
|
||||
assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions");
|
||||
|
||||
|
||||
// Steps 4-6.
|
||||
const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1);
|
||||
const mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault);
|
||||
const mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20);
|
||||
|
||||
// Steps 7-8.
|
||||
let mnsd = options.minimumSignificantDigits;
|
||||
let mxsd = options.maximumSignificantDigits;
|
||||
|
||||
// Steps 9-11.
|
||||
lazyData.minimumIntegerDigits = mnid;
|
||||
lazyData.minimumFractionDigits = mnfd;
|
||||
lazyData.maximumFractionDigits = mxfd;
|
||||
|
||||
// Step 12.
|
||||
if (mnsd !== undefined || mxsd !== undefined) {
|
||||
mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1);
|
||||
mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21);
|
||||
lazyData.minimumSignificantDigits = mnsd;
|
||||
lazyData.maximumSignificantDigits = mxsd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes an object as a NumberFormat.
|
||||
@ -1885,7 +1923,7 @@ function InitializeNumberFormat(numberFormat, locales, options) {
|
||||
// }
|
||||
//
|
||||
// Note that lazy data is only installed as a final step of initialization,
|
||||
// so every Collator lazy data object has *all* these properties, never a
|
||||
// so every NumberFormat lazy data object has *all* these properties, never a
|
||||
// subset of them.
|
||||
var lazyNumberFormatData = std_Object_create(null);
|
||||
|
||||
@ -1915,11 +1953,11 @@ function InitializeNumberFormat(numberFormat, locales, options) {
|
||||
opt.localeMatcher = matcher;
|
||||
|
||||
// Compute formatting options.
|
||||
// Step 15.
|
||||
// Step 14.
|
||||
var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal");
|
||||
lazyNumberFormatData.style = s;
|
||||
|
||||
// Steps 17-20.
|
||||
// Steps 16-19.
|
||||
var c = GetOption(options, "currency", "string", undefined, undefined);
|
||||
if (c !== undefined && !IsWellFormedCurrencyCode(c))
|
||||
ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c);
|
||||
@ -1934,48 +1972,30 @@ function InitializeNumberFormat(numberFormat, locales, options) {
|
||||
cDigits = CurrencyDigits(c);
|
||||
}
|
||||
|
||||
// Step 21.
|
||||
// Step 20.
|
||||
var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol");
|
||||
if (s === "currency")
|
||||
lazyNumberFormatData.currencyDisplay = cd;
|
||||
|
||||
// Step 23.
|
||||
var mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1);
|
||||
lazyNumberFormatData.minimumIntegerDigits = mnid;
|
||||
// Steps 22-24.
|
||||
SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0);
|
||||
|
||||
// Steps 25-26.
|
||||
var mnfdDefault = (s === "currency") ? cDigits : 0;
|
||||
var mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault);
|
||||
lazyNumberFormatData.minimumFractionDigits = mnfd;
|
||||
|
||||
// Steps 28-29.
|
||||
var mxfdDefault;
|
||||
if (s === "currency")
|
||||
mxfdDefault = std_Math_max(mnfd, cDigits);
|
||||
else if (s === "percent")
|
||||
mxfdDefault = std_Math_max(mnfd, 0);
|
||||
else
|
||||
mxfdDefault = std_Math_max(mnfd, 3);
|
||||
var mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20, mxfdDefault);
|
||||
lazyNumberFormatData.maximumFractionDigits = mxfd;
|
||||
|
||||
// Steps 31-32.
|
||||
var mnsd = options.minimumSignificantDigits;
|
||||
var mxsd = options.maximumSignificantDigits;
|
||||
|
||||
// Step 33.
|
||||
if (mnsd !== undefined || mxsd !== undefined) {
|
||||
mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1);
|
||||
mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21);
|
||||
lazyNumberFormatData.minimumSignificantDigits = mnsd;
|
||||
lazyNumberFormatData.maximumSignificantDigits = mxsd;
|
||||
// Step 25.
|
||||
if (lazyNumberFormatData.maximumFractionDigits === undefined) {
|
||||
let mxfdDefault = s === "currency"
|
||||
? cDigits
|
||||
: s === "percent"
|
||||
? 0
|
||||
: 3;
|
||||
lazyNumberFormatData.maximumFractionDigits =
|
||||
std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault);
|
||||
}
|
||||
|
||||
// Step 34.
|
||||
// Steps 26.
|
||||
var g = GetOption(options, "useGrouping", "boolean", undefined, true);
|
||||
lazyNumberFormatData.useGrouping = g;
|
||||
|
||||
// Step 43.
|
||||
// Steps 35-36.
|
||||
//
|
||||
// We've done everything that must be done now: mark the lazy data as fully
|
||||
// computed and install it.
|
||||
@ -2664,7 +2684,6 @@ function ToDateTimeOptions(options, required, defaults) {
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares the date and time components requested by options with the available
|
||||
* date and time formats in formats, and selects the best match according
|
||||
@ -2985,6 +3004,234 @@ function resolveICUPattern(pattern, result) {
|
||||
}
|
||||
}
|
||||
|
||||
/********** Intl.PluralRules **********/
|
||||
|
||||
/**
|
||||
* PluralRules internal properties.
|
||||
*
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.3.3.
|
||||
*/
|
||||
var pluralRulesInternalProperties = {
|
||||
_availableLocales: null,
|
||||
availableLocales: function()
|
||||
{
|
||||
var locales = this._availableLocales;
|
||||
if (locales)
|
||||
return locales;
|
||||
|
||||
locales = intl_PluralRules_availableLocales();
|
||||
addSpecialMissingLanguageTags(locales);
|
||||
return (this._availableLocales = locales);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute an internal properties object from |lazyPluralRulesData|.
|
||||
*/
|
||||
function resolvePluralRulesInternals(lazyPluralRulesData) {
|
||||
assert(IsObject(lazyPluralRulesData), "lazy data not an object?");
|
||||
|
||||
var internalProps = std_Object_create(null);
|
||||
|
||||
var requestedLocales = lazyPluralRulesData.requestedLocales;
|
||||
|
||||
var PluralRules = pluralRulesInternalProperties;
|
||||
|
||||
// Step 13.
|
||||
const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules),
|
||||
lazyPluralRulesData.requestedLocales,
|
||||
lazyPluralRulesData.opt,
|
||||
noRelevantExtensionKeys, undefined);
|
||||
|
||||
// Step 14.
|
||||
internalProps.locale = r.locale;
|
||||
internalProps.type = lazyPluralRulesData.type;
|
||||
|
||||
internalProps.pluralCategories = intl_GetPluralCategories(
|
||||
internalProps.locale,
|
||||
internalProps.type);
|
||||
|
||||
internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits;
|
||||
internalProps.minimumFractionDigits = lazyPluralRulesData.minimumFractionDigits;
|
||||
internalProps.maximumFractionDigits = lazyPluralRulesData.maximumFractionDigits;
|
||||
|
||||
if ("minimumSignificantDigits" in lazyPluralRulesData) {
|
||||
assert("maximumSignificantDigits" in lazyPluralRulesData, "min/max sig digits mismatch");
|
||||
internalProps.minimumSignificantDigits = lazyPluralRulesData.minimumSignificantDigits;
|
||||
internalProps.maximumSignificantDigits = lazyPluralRulesData.maximumSignificantDigits;
|
||||
}
|
||||
|
||||
return internalProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing the PluralRules internal properties of |obj|,
|
||||
* or throws a TypeError if |obj| isn't PluralRules-initialized.
|
||||
*/
|
||||
function getPluralRulesInternals(obj, methodName) {
|
||||
var internals = getIntlObjectInternals(obj, "PluralRules", methodName);
|
||||
assert(internals.type === "PluralRules", "bad type escaped getIntlObjectInternals");
|
||||
|
||||
var internalProps = maybeInternalProperties(internals);
|
||||
if (internalProps)
|
||||
return internalProps;
|
||||
|
||||
internalProps = resolvePluralRulesInternals(internals.lazyData);
|
||||
setInternalProperties(internals, internalProps);
|
||||
return internalProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an object as a PluralRules.
|
||||
*
|
||||
* This method is complicated a moderate bit by its implementing initialization
|
||||
* as a *lazy* concept. Everything that must happen now, does -- but we defer
|
||||
* all the work we can until the object is actually used as a PluralRules.
|
||||
* This later work occurs in |resolvePluralRulesInternals|; steps not noted
|
||||
* here occur there.
|
||||
*
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.1.1.
|
||||
*/
|
||||
function InitializePluralRules(pluralRules, locales, options) {
|
||||
assert(IsObject(pluralRules), "InitializePluralRules");
|
||||
|
||||
// Step 1.
|
||||
if (isInitializedIntlObject(pluralRules))
|
||||
ThrowTypeError(JSMSG_INTL_OBJECT_REINITED);
|
||||
|
||||
let internals = initializeIntlObject(pluralRules);
|
||||
|
||||
// Lazy PluralRules data has the following structure:
|
||||
//
|
||||
// {
|
||||
// requestedLocales: List of locales,
|
||||
// type: "cardinal" / "ordinal",
|
||||
//
|
||||
// opt: // opt object computer in InitializePluralRules
|
||||
// {
|
||||
// localeMatcher: "lookup" / "best fit",
|
||||
// }
|
||||
//
|
||||
// minimumIntegerDigits: integer ∈ [1, 21],
|
||||
// minimumFractionDigits: integer ∈ [0, 20],
|
||||
// maximumFractionDigits: integer ∈ [0, 20],
|
||||
//
|
||||
// // optional
|
||||
// minimumSignificantDigits: integer ∈ [1, 21],
|
||||
// maximumSignificantDigits: integer ∈ [1, 21],
|
||||
// }
|
||||
//
|
||||
// Note that lazy data is only installed as a final step of initialization,
|
||||
// so every PluralRules lazy data object has *all* these properties, never a
|
||||
// subset of them.
|
||||
const lazyPluralRulesData = std_Object_create(null);
|
||||
|
||||
// Step 3.
|
||||
let requestedLocales = CanonicalizeLocaleList(locales);
|
||||
lazyPluralRulesData.requestedLocales = requestedLocales;
|
||||
|
||||
// Steps 4-5.
|
||||
if (options === undefined)
|
||||
options = {};
|
||||
else
|
||||
options = ToObject(options);
|
||||
|
||||
// Step 6.
|
||||
const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal");
|
||||
lazyPluralRulesData.type = type;
|
||||
|
||||
// Step 8.
|
||||
let opt = new Record();
|
||||
lazyPluralRulesData.opt = opt;
|
||||
|
||||
// Steps 9-10.
|
||||
let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit");
|
||||
opt.localeMatcher = matcher;
|
||||
|
||||
|
||||
// Step 11.
|
||||
SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0);
|
||||
|
||||
// Step 12.
|
||||
if (lazyPluralRulesData.maximumFractionDigits === undefined) {
|
||||
lazyPluralRulesData.maximumFractionDigits =
|
||||
std_Math_max(lazyPluralRulesData.minimumFractionDigits, 3);
|
||||
}
|
||||
|
||||
setLazyData(internals, "PluralRules", lazyPluralRulesData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of the given locale list for which this locale list has a
|
||||
* matching (possibly fallback) locale. Locales appear in the same order in the
|
||||
* returned list as in the input list.
|
||||
*
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.3.2.
|
||||
*/
|
||||
function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) {
|
||||
var options = arguments.length > 1 ? arguments[1] : undefined;
|
||||
|
||||
// Step 1.
|
||||
var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales,
|
||||
pluralRulesInternalProperties);
|
||||
// Step 2.
|
||||
let requestedLocales = CanonicalizeLocaleList(locales);
|
||||
|
||||
// Step 3.
|
||||
return SupportedLocales(availableLocales, requestedLocales, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a String value representing the plural category matching
|
||||
* the number passed as value according to the
|
||||
* effective locale and the formatting options of this PluralRules.
|
||||
*
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.4.3.
|
||||
*/
|
||||
function Intl_PluralRules_select(value) {
|
||||
// Step 1.
|
||||
let pluralRules = this;
|
||||
// Step 2.
|
||||
let internals = getPluralRulesInternals(pluralRules, "select");
|
||||
|
||||
// Steps 3-4.
|
||||
let n = ToNumber(value);
|
||||
|
||||
// Step 5.
|
||||
return intl_SelectPluralRule(pluralRules, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resolved options for a PluralRules object.
|
||||
*
|
||||
* Spec: ECMAScript 402 API, PluralRules, 1.4.4.
|
||||
*/
|
||||
function Intl_PluralRules_resolvedOptions() {
|
||||
var internals = getPluralRulesInternals(this, "resolvedOptions");
|
||||
|
||||
var result = {
|
||||
locale: internals.locale,
|
||||
type: internals.type,
|
||||
pluralCategories: callFunction(std_Array_slice, internals.pluralCategories, 0),
|
||||
minimumIntegerDigits: internals.minimumIntegerDigits,
|
||||
minimumFractionDigits: internals.minimumFractionDigits,
|
||||
maximumFractionDigits: internals.maximumFractionDigits,
|
||||
};
|
||||
|
||||
var optionalProperties = [
|
||||
"minimumSignificantDigits",
|
||||
"maximumSignificantDigits"
|
||||
];
|
||||
|
||||
for (var i = 0; i < optionalProperties.length; i++) {
|
||||
var p = optionalProperties[i];
|
||||
if (callFunction(std_Object_hasOwnProperty, internals, p))
|
||||
_DefineDataProperty(result, p, internals[p]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function Intl_getCanonicalLocales(locales) {
|
||||
let codes = CanonicalizeLocaleList(locales);
|
||||
let result = [];
|
||||
|
13
js/src/tests/Intl/NumberFormat/options-emulate-undefined.js
Normal file
13
js/src/tests/Intl/NumberFormat/options-emulate-undefined.js
Normal file
@ -0,0 +1,13 @@
|
||||
// |reftest| skip-if(!this.hasOwnProperty("Intl"))
|
||||
/* 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/. */
|
||||
|
||||
// objectEmulatingUndefined is only available when running tests in the shell,
|
||||
// not the browser
|
||||
if (typeof objectEmulatingUndefined === "function") {
|
||||
let nf = new Intl.NumberFormat('en-US', objectEmulatingUndefined());
|
||||
}
|
||||
|
||||
if (typeof reportCompare === "function")
|
||||
reportCompare(true, true);
|
0
js/src/tests/Intl/PluralRules/browser.js
Normal file
0
js/src/tests/Intl/PluralRules/browser.js
Normal file
21
js/src/tests/Intl/PluralRules/pluralrules.js
Normal file
21
js/src/tests/Intl/PluralRules/pluralrules.js
Normal file
@ -0,0 +1,21 @@
|
||||
// |reftest| skip-if(!this.hasOwnProperty("Intl"))
|
||||
/* 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/. */
|
||||
|
||||
// Tests the format function with a diverse set of locales and options.
|
||||
|
||||
var pr;
|
||||
|
||||
pr = new Intl.PluralRules("en-us");
|
||||
assertEq(pr.resolvedOptions().locale, "en-US");
|
||||
assertEq(pr.resolvedOptions().type, "cardinal");
|
||||
assertEq(pr.resolvedOptions().pluralCategories.length, 2);
|
||||
|
||||
pr = new Intl.PluralRules("de", {type: 'cardinal'});
|
||||
assertEq(pr.resolvedOptions().pluralCategories.length, 2);
|
||||
|
||||
pr = new Intl.PluralRules("de", {type: 'ordinal'});
|
||||
assertEq(pr.resolvedOptions().pluralCategories.length, 1);
|
||||
|
||||
reportCompare(0, 0, 'ok');
|
60
js/src/tests/Intl/PluralRules/select.js
Normal file
60
js/src/tests/Intl/PluralRules/select.js
Normal file
@ -0,0 +1,60 @@
|
||||
// |reftest| skip-if(!this.hasOwnProperty("Intl"))
|
||||
/* 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/. */
|
||||
|
||||
// Tests the format function with a diverse set of locales and options.
|
||||
|
||||
var pr;
|
||||
|
||||
pr = new Intl.PluralRules("en-us");
|
||||
assertEq(pr.select(0), "other");
|
||||
assertEq(pr.select(0.5), "other");
|
||||
assertEq(pr.select(1.2), "other");
|
||||
assertEq(pr.select(1.5), "other");
|
||||
assertEq(pr.select(1.7), "other");
|
||||
assertEq(pr.select(-1), "one");
|
||||
assertEq(pr.select(1), "one");
|
||||
assertEq(pr.select("1"), "one");
|
||||
assertEq(pr.select(123456789.123456789), "other");
|
||||
|
||||
pr = new Intl.PluralRules("de", {type: "cardinal"});
|
||||
assertEq(pr.select(0), "other");
|
||||
assertEq(pr.select(0.5), "other");
|
||||
assertEq(pr.select(1.2), "other");
|
||||
assertEq(pr.select(1.5), "other");
|
||||
assertEq(pr.select(1.7), "other");
|
||||
assertEq(pr.select(-1), "one");
|
||||
|
||||
pr = new Intl.PluralRules("de", {type: "ordinal"});
|
||||
assertEq(pr.select(0), "other");
|
||||
assertEq(pr.select(0.5), "other");
|
||||
assertEq(pr.select(1.2), "other");
|
||||
assertEq(pr.select(1.5), "other");
|
||||
assertEq(pr.select(1.7), "other");
|
||||
assertEq(pr.select(-1), "other");
|
||||
|
||||
pr = new Intl.PluralRules("pl", {type: "cardinal"});
|
||||
assertEq(pr.select(0), "many");
|
||||
assertEq(pr.select(0.5), "other");
|
||||
assertEq(pr.select(1), "one");
|
||||
|
||||
pr = new Intl.PluralRules("pl", {type: "cardinal", maximumFractionDigits: 0});
|
||||
assertEq(pr.select(1.1), "one");
|
||||
|
||||
pr = new Intl.PluralRules("pl", {type: "cardinal", maximumFractionDigits: 1});
|
||||
assertEq(pr.select(1.1), "other");
|
||||
|
||||
var weirdCases = [
|
||||
NaN,
|
||||
Infinity,
|
||||
"word",
|
||||
[0,2],
|
||||
{},
|
||||
];
|
||||
|
||||
for (let c of weirdCases) {
|
||||
assertEq(pr.select(c), "other");
|
||||
};
|
||||
|
||||
reportCompare(0, 0, 'ok');
|
0
js/src/tests/Intl/PluralRules/shell.js
Normal file
0
js/src/tests/Intl/PluralRules/shell.js
Normal file
373
js/src/tests/Intl/PluralRules/supportedLocalesOf.js
Normal file
373
js/src/tests/Intl/PluralRules/supportedLocalesOf.js
Normal file
@ -0,0 +1,373 @@
|
||||
// |reftest| skip-if(!this.hasOwnProperty("Intl")||xulRuntime.shell)
|
||||
// -- test in browser only that ICU has locale data for all Mozilla languages
|
||||
|
||||
/* 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/. */
|
||||
|
||||
// This array contains the locales that ICU supports in
|
||||
// number formatting whose languages Mozilla localizes Firefox into.
|
||||
// Current as of ICU 50.1.2 and Firefox March 2013.
|
||||
var locales = [
|
||||
"af",
|
||||
"af-NA",
|
||||
"af-ZA",
|
||||
"ar",
|
||||
"ar-001",
|
||||
"ar-AE",
|
||||
"ar-BH",
|
||||
"ar-DJ",
|
||||
"ar-DZ",
|
||||
"ar-EG",
|
||||
"ar-EH",
|
||||
"ar-ER",
|
||||
"ar-IL",
|
||||
"ar-IQ",
|
||||
"ar-JO",
|
||||
"ar-KM",
|
||||
"ar-KW",
|
||||
"ar-LB",
|
||||
"ar-LY",
|
||||
"ar-MA",
|
||||
"ar-MR",
|
||||
"ar-OM",
|
||||
"ar-PS",
|
||||
"ar-QA",
|
||||
"ar-SA",
|
||||
"ar-SD",
|
||||
"ar-SO",
|
||||
"ar-SY",
|
||||
"ar-TD",
|
||||
"ar-TN",
|
||||
"ar-YE",
|
||||
"as",
|
||||
"as-IN",
|
||||
"be",
|
||||
"be-BY",
|
||||
"bg",
|
||||
"bg-BG",
|
||||
"bn",
|
||||
"bn-BD",
|
||||
"bn-IN",
|
||||
"br",
|
||||
"br-FR",
|
||||
"bs",
|
||||
"bs-Cyrl",
|
||||
"bs-Cyrl-BA",
|
||||
"bs-Latn",
|
||||
"bs-Latn-BA",
|
||||
"ca",
|
||||
"ca-AD",
|
||||
"ca-ES",
|
||||
"cs",
|
||||
"cs-CZ",
|
||||
"cy",
|
||||
"cy-GB",
|
||||
"da",
|
||||
"da-DK",
|
||||
"de",
|
||||
"de-AT",
|
||||
"de-BE",
|
||||
"de-CH",
|
||||
"de-DE",
|
||||
"de-LI",
|
||||
"de-LU",
|
||||
"el",
|
||||
"el-CY",
|
||||
"el-GR",
|
||||
"en",
|
||||
"en-150",
|
||||
"en-AG",
|
||||
"en-AS",
|
||||
"en-AU",
|
||||
"en-BB",
|
||||
"en-BE",
|
||||
"en-BM",
|
||||
"en-BS",
|
||||
"en-BW",
|
||||
"en-BZ",
|
||||
"en-CA",
|
||||
"en-CM",
|
||||
"en-DM",
|
||||
"en-FJ",
|
||||
"en-FM",
|
||||
"en-GB",
|
||||
"en-GD",
|
||||
"en-GG",
|
||||
"en-GH",
|
||||
"en-GI",
|
||||
"en-GM",
|
||||
"en-GU",
|
||||
"en-GY",
|
||||
"en-HK",
|
||||
"en-IE",
|
||||
"en-IM",
|
||||
"en-IN",
|
||||
"en-JE",
|
||||
"en-JM",
|
||||
"en-KE",
|
||||
"en-KI",
|
||||
"en-KN",
|
||||
"en-KY",
|
||||
"en-LC",
|
||||
"en-LR",
|
||||
"en-LS",
|
||||
"en-MG",
|
||||
"en-MH",
|
||||
"en-MP",
|
||||
"en-MT",
|
||||
"en-MU",
|
||||
"en-MW",
|
||||
"en-NA",
|
||||
"en-NG",
|
||||
"en-NZ",
|
||||
"en-PG",
|
||||
"en-PH",
|
||||
"en-PK",
|
||||
"en-PR",
|
||||
"en-PW",
|
||||
"en-SB",
|
||||
"en-SC",
|
||||
"en-SG",
|
||||
"en-SL",
|
||||
"en-SS",
|
||||
"en-SZ",
|
||||
"en-TC",
|
||||
"en-TO",
|
||||
"en-TT",
|
||||
"en-TZ",
|
||||
"en-UG",
|
||||
"en-UM",
|
||||
"en-US",
|
||||
"en-US-posix",
|
||||
"en-VC",
|
||||
"en-VG",
|
||||
"en-VI",
|
||||
"en-VU",
|
||||
"en-WS",
|
||||
"en-ZA",
|
||||
"en-ZM",
|
||||
"en-ZW",
|
||||
"eo",
|
||||
"es",
|
||||
"es-419",
|
||||
"es-AR",
|
||||
"es-BO",
|
||||
"es-CL",
|
||||
"es-CO",
|
||||
"es-CR",
|
||||
"es-CU",
|
||||
"es-DO",
|
||||
"es-EA",
|
||||
"es-EC",
|
||||
"es-ES",
|
||||
"es-GQ",
|
||||
"es-GT",
|
||||
"es-HN",
|
||||
"es-IC",
|
||||
"es-MX",
|
||||
"es-NI",
|
||||
"es-PA",
|
||||
"es-PE",
|
||||
"es-PH",
|
||||
"es-PR",
|
||||
"es-PY",
|
||||
"es-SV",
|
||||
"es-US",
|
||||
"es-UY",
|
||||
"es-VE",
|
||||
"et",
|
||||
"et-EE",
|
||||
"eu",
|
||||
"eu-ES",
|
||||
"fa",
|
||||
"fa-AF",
|
||||
"fa-IR",
|
||||
"ff",
|
||||
"ff-SN",
|
||||
"fi",
|
||||
"fi-FI",
|
||||
"fr",
|
||||
"fr-BE",
|
||||
"fr-BF",
|
||||
"fr-BI",
|
||||
"fr-BJ",
|
||||
"fr-BL",
|
||||
"fr-CA",
|
||||
"fr-CD",
|
||||
"fr-CF",
|
||||
"fr-CG",
|
||||
"fr-CH",
|
||||
"fr-CI",
|
||||
"fr-CM",
|
||||
"fr-DJ",
|
||||
"fr-DZ",
|
||||
"fr-FR",
|
||||
"fr-GA",
|
||||
"fr-GF",
|
||||
"fr-GN",
|
||||
"fr-GP",
|
||||
"fr-GQ",
|
||||
"fr-HT",
|
||||
"fr-KM",
|
||||
"fr-LU",
|
||||
"fr-MA",
|
||||
"fr-MC",
|
||||
"fr-MF",
|
||||
"fr-MG",
|
||||
"fr-ML",
|
||||
"fr-MQ",
|
||||
"fr-MR",
|
||||
"fr-MU",
|
||||
"fr-NC",
|
||||
"fr-NE",
|
||||
"fr-PF",
|
||||
"fr-RE",
|
||||
"fr-RW",
|
||||
"fr-SC",
|
||||
"fr-SN",
|
||||
"fr-SY",
|
||||
"fr-TD",
|
||||
"fr-TG",
|
||||
"fr-TN",
|
||||
"fr-VU",
|
||||
"fr-YT",
|
||||
"ga",
|
||||
"ga-IE",
|
||||
"gl",
|
||||
"gl-ES",
|
||||
"gu",
|
||||
"gu-IN",
|
||||
"he",
|
||||
"he-IL",
|
||||
"hi",
|
||||
"hi-IN",
|
||||
"hr",
|
||||
"hr-BA",
|
||||
"hr-HR",
|
||||
"hu",
|
||||
"hu-HU",
|
||||
"hy",
|
||||
"hy-AM",
|
||||
"id",
|
||||
"id-ID",
|
||||
"is",
|
||||
"is-IS",
|
||||
"it",
|
||||
"it-CH",
|
||||
"it-IT",
|
||||
"it-SM",
|
||||
"ja",
|
||||
"ja-JP",
|
||||
"kk",
|
||||
"kk-Cyrl",
|
||||
"kk-Cyrl-KZ",
|
||||
"km",
|
||||
"km-KH",
|
||||
"kn",
|
||||
"kn-IN",
|
||||
"ko",
|
||||
"ko-KP",
|
||||
"ko-KR",
|
||||
"lt",
|
||||
"lt-LT",
|
||||
"lv",
|
||||
"lv-LV",
|
||||
"mk",
|
||||
"mk-MK",
|
||||
"ml",
|
||||
"ml-IN",
|
||||
"mr",
|
||||
"mr-IN",
|
||||
"nb",
|
||||
"nb-NO",
|
||||
"nl",
|
||||
"nl-AW",
|
||||
"nl-BE",
|
||||
"nl-CW",
|
||||
"nl-NL",
|
||||
"nl-SR",
|
||||
"nl-SX",
|
||||
"nn",
|
||||
"nn-NO",
|
||||
"or",
|
||||
"or-IN",
|
||||
"pa",
|
||||
"pa-Arab",
|
||||
"pa-Arab-PK",
|
||||
"pa-Guru",
|
||||
"pa-Guru-IN",
|
||||
"pl",
|
||||
"pl-PL",
|
||||
"pt",
|
||||
"pt-AO",
|
||||
"pt-BR",
|
||||
"pt-CV",
|
||||
"pt-GW",
|
||||
"pt-MO",
|
||||
"pt-MZ",
|
||||
"pt-PT",
|
||||
"pt-ST",
|
||||
"pt-TL",
|
||||
"rm",
|
||||
"rm-CH",
|
||||
"ro",
|
||||
"ro-MD",
|
||||
"ro-RO",
|
||||
"ru",
|
||||
"ru-BY",
|
||||
"ru-KG",
|
||||
"ru-KZ",
|
||||
"ru-MD",
|
||||
"ru-RU",
|
||||
"ru-UA",
|
||||
"si",
|
||||
"si-LK",
|
||||
"sk",
|
||||
"sk-SK",
|
||||
"sl",
|
||||
"sl-SI",
|
||||
"sq",
|
||||
"sq-AL",
|
||||
"sq-MK",
|
||||
"sr",
|
||||
"sr-Cyrl",
|
||||
"sr-Cyrl-BA",
|
||||
"sr-Cyrl-ME",
|
||||
"sr-Cyrl-RS",
|
||||
"sr-Latn",
|
||||
"sr-Latn-BA",
|
||||
"sr-Latn-ME",
|
||||
"sr-Latn-RS",
|
||||
"sv",
|
||||
"sv-AX",
|
||||
"sv-FI",
|
||||
"sv-SE",
|
||||
"te",
|
||||
"te-IN",
|
||||
"th",
|
||||
"th-TH",
|
||||
"tr",
|
||||
"tr-CY",
|
||||
"tr-TR",
|
||||
"uk",
|
||||
"uk-UA",
|
||||
"vi",
|
||||
"vi-VN",
|
||||
"zh",
|
||||
"zh-Hans",
|
||||
"zh-Hans-CN",
|
||||
"zh-Hans-HK",
|
||||
"zh-Hans-MO",
|
||||
"zh-Hans-SG",
|
||||
"zh-Hant",
|
||||
"zh-Hant-HK",
|
||||
"zh-Hant-MO",
|
||||
"zh-Hant-TW",
|
||||
];
|
||||
|
||||
const result = Intl.PluralRules.supportedLocalesOf(locales);
|
||||
|
||||
assertEqArray(locales, result);
|
||||
|
||||
reportCompare(0, 0, 'ok');
|
@ -157,6 +157,7 @@
|
||||
macro(InitializeCollator, InitializeCollator, "InitializeCollator") \
|
||||
macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \
|
||||
macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \
|
||||
macro(InitializePluralRules, InitializePluralRules, "InitializePluralRules") \
|
||||
macro(innermost, innermost, "innermost") \
|
||||
macro(inNursery, inNursery, "inNursery") \
|
||||
macro(input, input, "input") \
|
||||
@ -248,6 +249,8 @@
|
||||
macro(parseInt, parseInt, "parseInt") \
|
||||
macro(pattern, pattern, "pattern") \
|
||||
macro(pending, pending, "pending") \
|
||||
macro(PluralRules, PluralRules, "PluralRules") \
|
||||
macro(PluralRulesSelect, PluralRulesSelect, "Intl_PluralRules_Select") \
|
||||
macro(percentSign, percentSign, "percentSign") \
|
||||
macro(plusSign, plusSign, "plusSign") \
|
||||
macro(preventExtensions, preventExtensions, "preventExtensions") \
|
||||
|
@ -104,6 +104,7 @@ class GlobalObject : public NativeObject
|
||||
COLLATOR_PROTO,
|
||||
NUMBER_FORMAT_PROTO,
|
||||
DATE_TIME_FORMAT_PROTO,
|
||||
PLURAL_RULES_PROTO,
|
||||
MODULE_PROTO,
|
||||
IMPORT_ENTRY_PROTO,
|
||||
EXPORT_ENTRY_PROTO,
|
||||
@ -486,6 +487,10 @@ class GlobalObject : public NativeObject
|
||||
return getOrCreateObject(cx, DATE_TIME_FORMAT_PROTO, initIntlObject);
|
||||
}
|
||||
|
||||
JSObject* getOrCreatePluralRulesPrototype(JSContext* cx) {
|
||||
return getOrCreateObject(cx, PLURAL_RULES_PROTO, initIntlObject);
|
||||
}
|
||||
|
||||
static bool ensureModulePrototypesCreated(JSContext *cx, Handle<GlobalObject*> global);
|
||||
|
||||
JSObject* maybeGetModulePrototype() {
|
||||
|
@ -2523,6 +2523,9 @@ static const JSFunctionSpec intrinsic_functions[] = {
|
||||
JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0),
|
||||
JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0),
|
||||
JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0),
|
||||
JS_FN("intl_PluralRules_availableLocales", intl_PluralRules_availableLocales, 0,0),
|
||||
JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2, 0),
|
||||
JS_FN("intl_SelectPluralRule", intl_SelectPluralRule, 2,0),
|
||||
|
||||
JS_INLINABLE_FN("IsRegExpObject",
|
||||
intrinsic_IsInstanceOfBuiltin<RegExpObject>, 1,0,
|
||||
|
@ -13,7 +13,6 @@
|
||||
#include "nsCSSPseudoElements.h"
|
||||
#include "nsGenericHTMLElement.h"
|
||||
#include "nsIEditor.h"
|
||||
#include "nsIEditorIMESupport.h"
|
||||
#include "nsIPhonetic.h"
|
||||
#include "nsTextFragment.h"
|
||||
#include "nsIDOMHTMLTextAreaElement.h"
|
||||
@ -1207,11 +1206,9 @@ nsTextControlFrame::GetPhonetic(nsAString& aPhonetic)
|
||||
nsresult rv = GetEditor(getter_AddRefs(editor));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
nsCOMPtr<nsIEditorIMESupport> imeSupport = do_QueryInterface(editor);
|
||||
if (imeSupport) {
|
||||
nsCOMPtr<nsIPhonetic> phonetic = do_QueryInterface(imeSupport);
|
||||
if (phonetic)
|
||||
phonetic->GetPhonetic(aPhonetic);
|
||||
nsCOMPtr<nsIPhonetic> phonetic = do_QueryInterface(editor);
|
||||
if (phonetic) {
|
||||
phonetic->GetPhonetic(aPhonetic);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ body { padding: 50px }
|
||||
height: 200px; width: 200px;
|
||||
backface-visibility: hidden;
|
||||
/* use a -99.9s delay to start at 99.9% and then move to 0% */
|
||||
animation: flip 100s -99.9s linear 2;
|
||||
animation: flip 100s -99.9s linear 2 paused;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -27,7 +27,13 @@ body { padding: 50px }
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById("test").addEventListener("animationiteration", IterationListener, false);
|
||||
document.getElementById("test").addEventListener("animationstart", StartListener, false);
|
||||
|
||||
function StartListener(event) {
|
||||
var test = document.getElementById("test");
|
||||
test.style.animationPlayState = 'running';
|
||||
test.addEventListener("animationiteration", IterationListener, false);
|
||||
}
|
||||
|
||||
function IterationListener(event) {
|
||||
setTimeout(RemoveReftestWait, 0);
|
||||
|
@ -18,7 +18,7 @@ body { padding: 50px }
|
||||
border: 1px solid black;
|
||||
transform-style: preserve-3d;
|
||||
/* use a -99.9s delay to start at 99.9% and then move to 0% */
|
||||
animation: spin 100s -99.9s linear 2;
|
||||
animation: spin 100s -99.9s linear 2 paused;
|
||||
}
|
||||
|
||||
#child {
|
||||
@ -39,7 +39,13 @@ body { padding: 50px }
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById("parent").addEventListener("animationiteration", IterationListener, false);
|
||||
document.getElementById("parent").addEventListener("animationstart", StartListener, false);
|
||||
|
||||
function StartListener(event) {
|
||||
var test = document.getElementById("parent");
|
||||
test.style.animationPlayState = 'running';
|
||||
test.addEventListener("animationiteration", IterationListener, false);
|
||||
}
|
||||
|
||||
function IterationListener(event) {
|
||||
setTimeout(RemoveReftestWait, 0);
|
||||
|
@ -129,6 +129,22 @@ ToPrimitive(nsCSSKeyword aKeyword)
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
HasAccumulateMatrix(const nsCSSValueList* aList)
|
||||
{
|
||||
const nsCSSValueList *item = aList;
|
||||
do {
|
||||
nsCSSKeyword func =
|
||||
nsStyleTransformMatrix::TransformFunctionOf(item->mValue.GetArrayValue());
|
||||
if (func == eCSSKeyword_accumulatematrix) {
|
||||
return true;
|
||||
}
|
||||
item = item->mNext;
|
||||
} while (item);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
TransformFunctionsMatch(nsCSSKeyword func1, nsCSSKeyword func2)
|
||||
{
|
||||
@ -3139,12 +3155,21 @@ StyleAnimationValue::AddWeighted(nsCSSPropertyID aProperty,
|
||||
if (result) {
|
||||
result->mValue.SetNoneValue();
|
||||
}
|
||||
} else if (HasAccumulateMatrix(list2)) {
|
||||
result = AddDifferentTransformLists(0, list2, aCoeff2, list2,
|
||||
eCSSKeyword_interpolatematrix);
|
||||
} else {
|
||||
result = AddTransformLists(0, list2, aCoeff2, list2);
|
||||
}
|
||||
} else {
|
||||
if (list2->mValue.GetUnit() == eCSSUnit_None) {
|
||||
result = AddTransformLists(0, list1, aCoeff1, list1);
|
||||
if (HasAccumulateMatrix(list1)) {
|
||||
result = AddDifferentTransformLists(0, list1,
|
||||
aCoeff1, list1,
|
||||
eCSSKeyword_interpolatematrix);
|
||||
} else {
|
||||
result = AddTransformLists(0, list1, aCoeff1, list1);
|
||||
}
|
||||
} else if (TransformFunctionListsMatch(list1, list2)) {
|
||||
result = AddTransformLists(aCoeff1, list1, aCoeff2, list2,
|
||||
eCSSKeyword_interpolatematrix);
|
||||
|
@ -33,11 +33,15 @@ using mozilla::dom::AnimationPlayState;
|
||||
using mozilla::dom::KeyframeEffectReadOnly;
|
||||
using mozilla::dom::CSSAnimation;
|
||||
|
||||
typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase;
|
||||
|
||||
namespace {
|
||||
|
||||
// Pair of an event message and elapsed time used when determining the set of
|
||||
// events to queue.
|
||||
typedef Pair<EventMessage, StickyTimeDuration> EventPair;
|
||||
struct AnimationEventParams {
|
||||
EventMessage mMessage;
|
||||
StickyTimeDuration mElapsedTime;
|
||||
TimeStamp mTimeStamp;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
@ -193,78 +197,94 @@ CSSAnimation::QueueEvents()
|
||||
return;
|
||||
}
|
||||
nsAnimationManager* manager = presContext->AnimationManager();
|
||||
|
||||
ComputedTiming computedTiming = mEffect->GetComputedTiming();
|
||||
|
||||
if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Null) {
|
||||
return; // do nothing
|
||||
ComputedTiming::AnimationPhase currentPhase = computedTiming.mPhase;
|
||||
uint64_t currentIteration = computedTiming.mCurrentIteration;
|
||||
if (currentPhase == mPreviousPhase &&
|
||||
currentIteration == mPreviousIteration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that script can change the start time, so we have to handle moving
|
||||
// backwards through the animation as well as forwards. An 'animationstart'
|
||||
// is dispatched if we enter the active phase (regardless if that is from
|
||||
// before or after the animation's active phase). An 'animationend' is
|
||||
// dispatched if we leave the active phase (regardless if that is to before
|
||||
// or after the animation's active phase).
|
||||
const StickyTimeDuration zeroDuration;
|
||||
StickyTimeDuration intervalStartTime =
|
||||
std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay),
|
||||
computedTiming.mActiveDuration),
|
||||
zeroDuration);
|
||||
StickyTimeDuration intervalEndTime =
|
||||
std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay),
|
||||
computedTiming.mActiveDuration),
|
||||
zeroDuration);
|
||||
|
||||
bool wasActive = mPreviousPhaseOrIteration != PREVIOUS_PHASE_BEFORE &&
|
||||
mPreviousPhaseOrIteration != PREVIOUS_PHASE_AFTER;
|
||||
bool isActive =
|
||||
computedTiming.mPhase == ComputedTiming::AnimationPhase::Active;
|
||||
bool isSameIteration =
|
||||
computedTiming.mCurrentIteration == mPreviousPhaseOrIteration;
|
||||
bool skippedActivePhase =
|
||||
(mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE &&
|
||||
computedTiming.mPhase == ComputedTiming::AnimationPhase::After) ||
|
||||
(mPreviousPhaseOrIteration == PREVIOUS_PHASE_AFTER &&
|
||||
computedTiming.mPhase == ComputedTiming::AnimationPhase::Before);
|
||||
bool skippedFirstIteration =
|
||||
isActive &&
|
||||
mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE &&
|
||||
computedTiming.mCurrentIteration > 0;
|
||||
uint64_t iterationBoundary = mPreviousIteration > currentIteration
|
||||
? currentIteration + 1
|
||||
: currentIteration;
|
||||
StickyTimeDuration iterationStartTime =
|
||||
computedTiming.mDuration.MultDouble(
|
||||
(iterationBoundary - computedTiming.mIterationStart));
|
||||
|
||||
MOZ_ASSERT(!skippedActivePhase || (!isActive && !wasActive),
|
||||
"skippedActivePhase only makes sense if we were & are inactive");
|
||||
TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
|
||||
TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
|
||||
TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
|
||||
|
||||
if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) {
|
||||
mPreviousPhaseOrIteration = PREVIOUS_PHASE_BEFORE;
|
||||
} else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) {
|
||||
mPreviousPhaseOrIteration = computedTiming.mCurrentIteration;
|
||||
} else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::After) {
|
||||
mPreviousPhaseOrIteration = PREVIOUS_PHASE_AFTER;
|
||||
AutoTArray<AnimationEventParams, 2> events;
|
||||
switch (mPreviousPhase) {
|
||||
case AnimationPhase::Null:
|
||||
case AnimationPhase::Before:
|
||||
if (currentPhase == AnimationPhase::Active) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationStart,
|
||||
intervalStartTime,
|
||||
startTimeStamp });
|
||||
} else if (currentPhase == AnimationPhase::After) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationStart,
|
||||
intervalStartTime,
|
||||
startTimeStamp });
|
||||
events.AppendElement(AnimationEventParams{ eAnimationEnd,
|
||||
intervalEndTime,
|
||||
endTimeStamp });
|
||||
}
|
||||
break;
|
||||
case AnimationPhase::Active:
|
||||
if (currentPhase == AnimationPhase::Before) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationEnd,
|
||||
intervalStartTime,
|
||||
startTimeStamp });
|
||||
} else if (currentPhase == AnimationPhase::Active) {
|
||||
// The currentIteration must have changed or element we would have
|
||||
// returned early above.
|
||||
MOZ_ASSERT(currentIteration != mPreviousIteration);
|
||||
events.AppendElement(AnimationEventParams{ eAnimationIteration,
|
||||
iterationStartTime,
|
||||
iterationTimeStamp });
|
||||
} else if (currentPhase == AnimationPhase::After) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationEnd,
|
||||
intervalEndTime,
|
||||
endTimeStamp });
|
||||
}
|
||||
break;
|
||||
case AnimationPhase::After:
|
||||
if (currentPhase == AnimationPhase::Before) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationStart,
|
||||
intervalEndTime,
|
||||
startTimeStamp});
|
||||
events.AppendElement(AnimationEventParams{ eAnimationEnd,
|
||||
intervalStartTime,
|
||||
endTimeStamp });
|
||||
} else if (currentPhase == AnimationPhase::Active) {
|
||||
events.AppendElement(AnimationEventParams{ eAnimationStart,
|
||||
intervalEndTime,
|
||||
endTimeStamp });
|
||||
}
|
||||
break;
|
||||
}
|
||||
mPreviousPhase = currentPhase;
|
||||
mPreviousIteration = currentIteration;
|
||||
|
||||
AutoTArray<EventPair, 2> events;
|
||||
StickyTimeDuration initialAdvance = StickyTimeDuration(InitialAdvance());
|
||||
StickyTimeDuration iterationStart = computedTiming.mDuration *
|
||||
computedTiming.mCurrentIteration;
|
||||
const StickyTimeDuration& activeDuration = computedTiming.mActiveDuration;
|
||||
|
||||
if (skippedFirstIteration) {
|
||||
// Notify animationstart and animationiteration in same tick.
|
||||
events.AppendElement(EventPair(eAnimationStart, initialAdvance));
|
||||
events.AppendElement(EventPair(eAnimationIteration,
|
||||
std::max(iterationStart, initialAdvance)));
|
||||
} else if (!wasActive && isActive) {
|
||||
events.AppendElement(EventPair(eAnimationStart, initialAdvance));
|
||||
} else if (wasActive && !isActive) {
|
||||
events.AppendElement(EventPair(eAnimationEnd, activeDuration));
|
||||
} else if (wasActive && isActive && !isSameIteration) {
|
||||
events.AppendElement(EventPair(eAnimationIteration, iterationStart));
|
||||
} else if (skippedActivePhase) {
|
||||
events.AppendElement(EventPair(eAnimationStart,
|
||||
std::min(initialAdvance, activeDuration)));
|
||||
events.AppendElement(EventPair(eAnimationEnd, activeDuration));
|
||||
} else {
|
||||
return; // No events need to be sent
|
||||
}
|
||||
|
||||
for (const EventPair& pair : events){
|
||||
for (const AnimationEventParams& event : events){
|
||||
manager->QueueEvent(
|
||||
AnimationEventInfo(owningElement, owningPseudoType,
|
||||
pair.first(), mAnimationName,
|
||||
pair.second(),
|
||||
ElapsedTimeToTimeStamp(pair.second()),
|
||||
event.mMessage, mAnimationName,
|
||||
event.mElapsedTime, event.mTimeStamp,
|
||||
this));
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,8 @@ public:
|
||||
, mIsStylePaused(false)
|
||||
, mPauseShouldStick(false)
|
||||
, mNeedsNewAnimationIndexWhenRun(false)
|
||||
, mPreviousPhaseOrIteration(PREVIOUS_PHASE_BEFORE)
|
||||
, mPreviousPhase(ComputedTiming::AnimationPhase::Null)
|
||||
, mPreviousIteration(0)
|
||||
{
|
||||
// We might need to drop this assertion once we add a script-accessible
|
||||
// constructor but for animations generated from CSS markup the
|
||||
@ -257,13 +258,10 @@ protected:
|
||||
// its animation index should be updated.
|
||||
bool mNeedsNewAnimationIndexWhenRun;
|
||||
|
||||
enum {
|
||||
PREVIOUS_PHASE_BEFORE = uint64_t(-1),
|
||||
PREVIOUS_PHASE_AFTER = uint64_t(-2)
|
||||
};
|
||||
// One of the PREVIOUS_PHASE_* constants, or an integer for the iteration
|
||||
// whose start we last notified on.
|
||||
uint64_t mPreviousPhaseOrIteration;
|
||||
// Phase and current iteration from the previous time we queued events.
|
||||
// This is used to determine what new events to dispatch.
|
||||
ComputedTiming::AnimationPhase mPreviousPhase;
|
||||
uint64_t mPreviousIteration;
|
||||
};
|
||||
|
||||
} /* namespace dom */
|
||||
|
@ -1226,9 +1226,6 @@ advance_clock(0); // complete pending animation start
|
||||
is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.4), 0.01,
|
||||
"large negative delay test at 0ms");
|
||||
check_events([{ type: 'animationstart', target: div,
|
||||
animationName: 'anim2', elapsedTime: 3.6,
|
||||
pseudoElement: "" },
|
||||
{ type: 'animationiteration', target: div,
|
||||
animationName: 'anim2', elapsedTime: 3.6,
|
||||
pseudoElement: "" }],
|
||||
"right after start in large negative delay test");
|
||||
|
@ -1407,9 +1407,6 @@ addAsyncAnimTest(function *() {
|
||||
omta_is_approx("opacity", gTF.ease_in(0.4), 0.01, RunningOn.Compositor,
|
||||
"large negative delay test at 0ms");
|
||||
check_events([{ type: 'animationstart', target: gDiv,
|
||||
animationName: 'anim2', elapsedTime: 3.6,
|
||||
pseudoElement: "" },
|
||||
{ type: 'animationiteration', target: gDiv,
|
||||
animationName: 'anim2', elapsedTime: 3.6,
|
||||
pseudoElement: "" }],
|
||||
"right after start in large negative delay test");
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -801,7 +801,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'widget/TabThumbnailWrapper.java',
|
||||
'widget/ThumbnailView.java',
|
||||
'widget/TouchDelegateWithReset.java',
|
||||
'widget/TwoWayView.java',
|
||||
'ZoomedView.java',
|
||||
]]
|
||||
# The following sources are checked in to version control but
|
||||
|
@ -3,11 +3,7 @@
|
||||
- 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/. -->
|
||||
|
||||
<!-- TwoWayView doesn't let us set the margin around items (except as
|
||||
gecko:itemMargin, but that doesn't increase the hit area) so we
|
||||
have to surround the main View by a ViewGroup to create a pressable margin.
|
||||
|
||||
Note: the layout_height values are shared with the parent
|
||||
<!-- Note: the layout_height values are shared with the parent
|
||||
View (browser_search at the time of this writing).
|
||||
|
||||
The actual width of the FrameLayout is calculated at runtime by the
|
||||
|
@ -3,9 +3,6 @@
|
||||
- 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/. -->
|
||||
|
||||
<!-- TwoWayView doesn't let us set the margin around items (except as
|
||||
gecko:itemMargin, but that doesn't increase the hit area) so we
|
||||
have to surround the main View by a ViewGroup to create a pressable margin. -->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="48dp"
|
||||
|
@ -100,13 +100,6 @@
|
||||
<attr name="autoUpdateTheme" format="boolean"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="TwoWayView">
|
||||
<attr name="android:orientation"/>
|
||||
<attr name="android:choiceMode"/>
|
||||
<attr name="android:listSelector"/>
|
||||
<attr name="android:drawSelectorOnTop"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="HomeListView">
|
||||
<!-- Draws a divider on top of the list, if true. Defaults to false. -->
|
||||
<attr name="topDivider" format="boolean"/>
|
||||
|
@ -233,9 +233,9 @@ var ActionBarHandler = {
|
||||
_clearSelection: function(element = this._targetElement, win = this._contentWindow) {
|
||||
// Commit edit compositions, and clear focus from editables.
|
||||
if (element) {
|
||||
let imeSupport = this._getEditor(element, win).QueryInterface(Ci.nsIEditorIMESupport);
|
||||
if (imeSupport.composing) {
|
||||
imeSupport.forceCompositionEnd();
|
||||
let editor = this._getEditor(element, win);
|
||||
if (editor.composing) {
|
||||
editor.forceCompositionEnd();
|
||||
}
|
||||
element.blur();
|
||||
}
|
||||
@ -346,9 +346,8 @@ var ActionBarHandler = {
|
||||
if (element) {
|
||||
// If we have an active composition string, commit it, and
|
||||
// ensure proper element focus.
|
||||
let imeSupport = ActionBarHandler._getEditor(element, win).
|
||||
QueryInterface(Ci.nsIEditorIMESupport);
|
||||
if (imeSupport.composing) {
|
||||
let editor = ActionBarHandler._getEditor(element, win)
|
||||
if (editor.composing) {
|
||||
element.blur();
|
||||
element.focus();
|
||||
}
|
||||
|
@ -4851,9 +4851,8 @@ var FormAssistant = {
|
||||
// If we have an active composition string, commit it before sending
|
||||
// the autocomplete event with the text that will replace it.
|
||||
try {
|
||||
let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
|
||||
if (imeEditor.composing)
|
||||
imeEditor.forceCompositionEnd();
|
||||
if (editableElement.editor.composing)
|
||||
editableElement.editor.forceCompositionEnd();
|
||||
} catch (e) {}
|
||||
|
||||
editableElement.setUserInput(aData);
|
||||
|
@ -109,21 +109,19 @@
|
||||
},
|
||||
|
||||
test_reflush_changes: function() {
|
||||
let inputIme = getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
|
||||
do_check_true(inputIme.composing);
|
||||
do_check_true(getEditor().composing);
|
||||
|
||||
// Ending the composition then setting the input value triggers the bug.
|
||||
inputIme.forceCompositionEnd();
|
||||
getEditor().forceCompositionEnd();
|
||||
setValue("good"); // Value that testInputConnection.java expects.
|
||||
setSelection(4);
|
||||
},
|
||||
|
||||
test_set_selection: function() {
|
||||
let inputIme = getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
|
||||
do_check_true(inputIme.composing);
|
||||
do_check_true(getEditor().composing);
|
||||
|
||||
// Ending the composition then setting the selection triggers the bug.
|
||||
inputIme.forceCompositionEnd();
|
||||
getEditor().forceCompositionEnd();
|
||||
setSelection(3); // Offsets that testInputConnection.java expects.
|
||||
},
|
||||
|
||||
|
@ -1157,6 +1157,7 @@ pref("dom.disable_open_click_delay", 1000);
|
||||
|
||||
pref("dom.storage.enabled", true);
|
||||
pref("dom.storage.default_quota", 5120);
|
||||
pref("dom.storage.testing", false);
|
||||
|
||||
pref("dom.send_after_paint_to_content", false);
|
||||
|
||||
@ -2210,6 +2211,10 @@ pref("services.blocklist.addons.collection", "addons");
|
||||
pref("services.blocklist.addons.checked", 0);
|
||||
pref("services.blocklist.plugins.collection", "plugins");
|
||||
pref("services.blocklist.plugins.checked", 0);
|
||||
pref("services.blocklist.pinning.enabled", true);
|
||||
pref("services.blocklist.pinning.bucket", "pinning");
|
||||
pref("services.blocklist.pinning.collection", "pins");
|
||||
pref("services.blocklist.pinning.checked", 0);
|
||||
pref("services.blocklist.gfx.collection", "gfx");
|
||||
pref("services.blocklist.gfx.checked", 0);
|
||||
|
||||
|
@ -54,6 +54,33 @@ Please commit or stash these changes before vendoring, or re-run with `--ignore-
|
||||
'''.format(files='\n'.join(sorted(modified))))
|
||||
sys.exit(1)
|
||||
|
||||
def check_openssl(self):
|
||||
'''
|
||||
Set environment flags for building with openssl.
|
||||
|
||||
MacOS doesn't include openssl, but the openssl-sys crate used by
|
||||
mach-vendor expects one of the system. It's common to have one
|
||||
installed in /usr/local/opt/openssl by homebrew, but custom link
|
||||
flags are necessary to build against it.
|
||||
'''
|
||||
|
||||
test_paths = ['/usr/include', '/usr/local/include']
|
||||
if any([os.path.exists(os.path.join(path, 'openssl/ssl.h')) for path in test_paths]):
|
||||
# Assume we can use one of these system headers.
|
||||
return None
|
||||
|
||||
if os.path.exists('/usr/local/opt/openssl/include/openssl/ssl.h'):
|
||||
# Found a likely homebrew install.
|
||||
self.log(logging.INFO, 'openssl', {},
|
||||
'Using OpenSSL in /usr/local/opt/openssl')
|
||||
return {
|
||||
'OPENSSL_INCLUDE_DIR': '/usr/local/opt/openssl/include',
|
||||
'DEP_OPENSSL_INCLUDE': '/usr/local/opt/openssl/include',
|
||||
}
|
||||
|
||||
self.log(logging.ERROR, 'openssl', {}, "OpenSSL not found!")
|
||||
return None
|
||||
|
||||
def vendor(self, ignore_modified=False):
|
||||
self.populate_logger()
|
||||
self.log_manager.enable_unstructured()
|
||||
@ -68,7 +95,9 @@ Please commit or stash these changes before vendoring, or re-run with `--ignore-
|
||||
have_vendor = any(l.strip() == 'vendor' for l in subprocess.check_output([cargo, '--list']).splitlines())
|
||||
if not have_vendor:
|
||||
self.log(logging.INFO, 'installing', {}, 'Installing cargo-vendor')
|
||||
self.run_process(args=[cargo, 'install', 'cargo-vendor'])
|
||||
env = self.check_openssl()
|
||||
self.run_process(args=[cargo, 'install', 'cargo-vendor'],
|
||||
append_env=env)
|
||||
else:
|
||||
self.log(logging.DEBUG, 'cargo_vendor', {}, 'cargo-vendor already intalled')
|
||||
vendor_dir = mozpath.join(self.topsrcdir, 'third_party/rust')
|
||||
|
@ -8,6 +8,7 @@ this.EXPORTED_SYMBOLS = ["AddonBlocklistClient",
|
||||
"GfxBlocklistClient",
|
||||
"OneCRLBlocklistClient",
|
||||
"PluginBlocklistClient",
|
||||
"PinningBlocklistClient",
|
||||
"FILENAME_ADDONS_JSON",
|
||||
"FILENAME_GFX_JSON",
|
||||
"FILENAME_PLUGINS_JSON"];
|
||||
@ -32,6 +33,10 @@ const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collec
|
||||
const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = "services.blocklist.addons.checked";
|
||||
const PREF_BLOCKLIST_PLUGINS_COLLECTION = "services.blocklist.plugins.collection";
|
||||
const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.checked";
|
||||
const PREF_BLOCKLIST_PINNING_ENABLED = "services.blocklist.pinning.enabled";
|
||||
const PREF_BLOCKLIST_PINNING_BUCKET = "services.blocklist.pinning.bucket";
|
||||
const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
|
||||
const PREF_BLOCKLIST_PINNING_CHECKED_SECONDS = "services.blocklist.pinning.checked";
|
||||
const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection";
|
||||
const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked";
|
||||
const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
|
||||
@ -82,9 +87,8 @@ function fetchRemoteCollection(collection) {
|
||||
* URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to
|
||||
* persist the local DB.
|
||||
*/
|
||||
function kintoClient(connection) {
|
||||
function kintoClient(connection, bucket) {
|
||||
let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
|
||||
|
||||
let config = {
|
||||
remote: base,
|
||||
@ -99,10 +103,11 @@ function kintoClient(connection) {
|
||||
|
||||
class BlocklistClient {
|
||||
|
||||
constructor(collectionName, lastCheckTimePref, processCallback, signerName) {
|
||||
constructor(collectionName, lastCheckTimePref, processCallback, bucketName, signerName) {
|
||||
this.collectionName = collectionName;
|
||||
this.lastCheckTimePref = lastCheckTimePref;
|
||||
this.processCallback = processCallback;
|
||||
this.bucketName = bucketName;
|
||||
this.signerName = signerName;
|
||||
}
|
||||
|
||||
@ -168,7 +173,7 @@ class BlocklistClient {
|
||||
let connection;
|
||||
try {
|
||||
connection = yield FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
|
||||
let db = kintoClient(connection);
|
||||
let db = kintoClient(connection, this.bucketName);
|
||||
let collection = db.collection(this.collectionName, opts);
|
||||
|
||||
let collectionLastModified = yield collection.db.getLastModified();
|
||||
@ -247,14 +252,54 @@ function* updateCertBlocklist(records) {
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent errors relating to individual blocklist entries from
|
||||
// causing sync to fail. At some point in the future, we may want to
|
||||
// accumulate telemetry on these failures.
|
||||
// causing sync to fail. We will accumulate telemetry on these failures in
|
||||
// bug 1254099.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
certList.saveEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the appropriate security pins based on records from the remote
|
||||
* collection.
|
||||
*
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updatePinningList(records) {
|
||||
if (Services.prefs.getBoolPref(PREF_BLOCKLIST_PINNING_ENABLED)) {
|
||||
const appInfo = Cc["@mozilla.org/xre/app-info;1"]
|
||||
.getService(Ci.nsIXULAppInfo);
|
||||
|
||||
const siteSecurityService = Cc["@mozilla.org/ssservice;1"]
|
||||
.getService(Ci.nsISiteSecurityService);
|
||||
|
||||
// clear the current preload list
|
||||
siteSecurityService.clearPreloads();
|
||||
|
||||
// write each KeyPin entry to the preload list
|
||||
for (let item of records) {
|
||||
try {
|
||||
const {pinType, pins=[], versions} = item;
|
||||
if (pinType == "KeyPin" && pins.length &&
|
||||
versions.indexOf(appInfo.version) != -1) {
|
||||
siteSecurityService.setKeyPins(item.hostName,
|
||||
item.includeSubdomains,
|
||||
item.expires,
|
||||
pins.length,
|
||||
pins, true);
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent errors relating to individual preload entries from causing
|
||||
// sync to fail. We will accumulate telemetry for such failures in bug
|
||||
// 1254099.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write list of records into JSON file, and notify nsBlocklistService.
|
||||
*
|
||||
@ -276,28 +321,39 @@ function* updateJSONBlocklist(filename, records) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.OneCRLBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION),
|
||||
PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS,
|
||||
updateCertBlocklist,
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET),
|
||||
"onecrl.content-signature.mozilla.org"
|
||||
);
|
||||
|
||||
this.AddonBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION),
|
||||
PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON)
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON),
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET)
|
||||
);
|
||||
|
||||
this.GfxBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION),
|
||||
PREF_BLOCKLIST_GFX_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON)
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON),
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET)
|
||||
);
|
||||
|
||||
this.PluginBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION),
|
||||
PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON)
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON),
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET)
|
||||
);
|
||||
|
||||
this.PinningPreloadClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_PINNING_COLLECTION),
|
||||
PREF_BLOCKLIST_PINNING_CHECKED_SECONDS,
|
||||
updatePinningList,
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_PINNING_BUCKET),
|
||||
"pinning-preload.content-signature.mozilla.org"
|
||||
);
|
||||
|
@ -13,7 +13,6 @@ const BlocklistClients = Cu.import("resource://services-common/blocklist-clients
|
||||
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_BLOCKLIST_CHANGES_PATH = "services.blocklist.changes.path";
|
||||
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
|
||||
const PREF_BLOCKLIST_LAST_UPDATE = "services.blocklist.last_update_seconds";
|
||||
const PREF_BLOCKLIST_LAST_ETAG = "services.blocklist.last_etag";
|
||||
const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
|
||||
@ -23,7 +22,8 @@ const gBlocklistClients = {
|
||||
[BlocklistClients.OneCRLBlocklistClient.collectionName]: BlocklistClients.OneCRLBlocklistClient,
|
||||
[BlocklistClients.AddonBlocklistClient.collectionName]: BlocklistClients.AddonBlocklistClient,
|
||||
[BlocklistClients.GfxBlocklistClient.collectionName]: BlocklistClients.GfxBlocklistClient,
|
||||
[BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient
|
||||
[BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient,
|
||||
[BlocklistClients.PinningPreloadClient.collectionName]: BlocklistClients.PinningPreloadClient
|
||||
};
|
||||
|
||||
// Add a blocklist client for testing purposes. Do not use for any other purpose
|
||||
@ -44,7 +44,6 @@ this.checkVersions = function() {
|
||||
// Right now, we only use the collection name and the last modified info
|
||||
let kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
|
||||
let blocklistsBucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
@ -82,14 +81,11 @@ this.checkVersions = function() {
|
||||
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
// Skip changes that don't concern configured blocklist bucket.
|
||||
if (collectionInfo.bucket != blocklistsBucket) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let collection = collectionInfo.collection;
|
||||
let client = gBlocklistClients[collection];
|
||||
if (client && client.maybeSync) {
|
||||
if (client &&
|
||||
client.bucketName == collectionInfo.bucket &&
|
||||
client.maybeSync) {
|
||||
let lastModified = 0;
|
||||
if (collectionInfo.last_modified) {
|
||||
lastModified = collectionInfo.last_modified;
|
||||
|
298
services/common/tests/unit/test_blocklist_pinning.js
Normal file
298
services/common/tests/unit/test_blocklist_pinning.js
Normal file
@ -0,0 +1,298 @@
|
||||
"use strict"
|
||||
|
||||
const { Constructor: CC } = Components;
|
||||
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js");
|
||||
|
||||
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
|
||||
const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
|
||||
const COLLECTION_NAME = "pins";
|
||||
const KINTO_STORAGE_PATH = "kinto.sqlite";
|
||||
|
||||
// First, we need to setup appInfo or we can't do version checks
|
||||
var id = "xpcshell@tests.mozilla.org";
|
||||
var appName = "XPCShell";
|
||||
var version = "1";
|
||||
var platformVersion = "1.9.2";
|
||||
Cu.import("resource://testing-common/AppInfo.jsm", this);
|
||||
|
||||
updateAppInfo({
|
||||
name: appName,
|
||||
ID: id,
|
||||
version: version,
|
||||
platformVersion: platformVersion ? platformVersion : "1.0",
|
||||
crashReporter: true,
|
||||
});
|
||||
|
||||
let server;
|
||||
|
||||
|
||||
function do_get_kinto_collection(connection, collectionName) {
|
||||
let config = {
|
||||
// Set the remote to be some server that will cause test failure when
|
||||
// hit since we should never hit the server directly (any non-local
|
||||
// request causes failure in tests), only via maybeSync()
|
||||
remote: "https://firefox.settings.services.mozilla.com/v1/",
|
||||
// Set up the adapter and bucket as normal
|
||||
adapter: FirefoxAdapter,
|
||||
adapterOptions: {sqliteHandle: connection},
|
||||
bucket: "pinning"
|
||||
};
|
||||
let kintoClient = new Kinto(config);
|
||||
return kintoClient.collection(collectionName);
|
||||
}
|
||||
|
||||
// Some simple tests to demonstrate that the core preload sync operations work
|
||||
// correctly and that simple kinto operations are working as expected.
|
||||
add_task(function* test_something(){
|
||||
// set the collection name explicitly - since there will be version
|
||||
// specific collection names in prefs
|
||||
Services.prefs.setCharPref(PREF_BLOCKLIST_PINNING_COLLECTION,
|
||||
COLLECTION_NAME);
|
||||
|
||||
const { PinningPreloadClient } = Cu.import("resource://services-common/blocklist-clients.js");
|
||||
|
||||
const configPath = "/v1/";
|
||||
const recordsPath = "/v1/buckets/pinning/collections/pins/records";
|
||||
|
||||
Services.prefs.setCharPref("services.settings.server",
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// register a handler
|
||||
function handleResponse (request, response) {
|
||||
try {
|
||||
const sample = getSampleResponse(request, server.identity.primaryPort);
|
||||
if (!sample) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sample.status.status,
|
||||
sample.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sample.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
response.setHeader("Date", (new Date()).toUTCString());
|
||||
|
||||
response.write(sample.responseBody);
|
||||
} catch (e) {
|
||||
do_print(e);
|
||||
}
|
||||
}
|
||||
server.registerPathHandler(configPath, handleResponse);
|
||||
server.registerPathHandler(recordsPath, handleResponse);
|
||||
|
||||
let sss = Cc["@mozilla.org/ssservice;1"]
|
||||
.getService(Ci.nsISiteSecurityService);
|
||||
|
||||
// ensure our pins are all missing before we start
|
||||
ok(!sss.isSecureHost(sss.HEADER_HPKP, "one.example.com", 0));
|
||||
ok(!sss.isSecureHost(sss.HEADER_HPKP, "two.example.com", 0));
|
||||
ok(!sss.isSecureHost(sss.HEADER_HPKP, "three.example.com", 0));
|
||||
|
||||
// Test an empty db populates
|
||||
let result = yield PinningPreloadClient.maybeSync(2000, Date.now());
|
||||
|
||||
let connection = yield FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
|
||||
|
||||
// Open the collection, verify it's been populated:
|
||||
// Our test data has a single record; it should be in the local collection
|
||||
let collection = do_get_kinto_collection(connection, COLLECTION_NAME);
|
||||
let list = yield collection.list();
|
||||
do_check_eq(list.data.length, 1);
|
||||
|
||||
// check that a pin exists for one.example.com
|
||||
ok(sss.isSecureHost(sss.HEADER_HPKP, "one.example.com", 0));
|
||||
|
||||
// Test the db is updated when we call again with a later lastModified value
|
||||
result = yield PinningPreloadClient.maybeSync(4000, Date.now());
|
||||
|
||||
// Open the collection, verify it's been updated:
|
||||
// Our data now has three new records; all should be in the local collection
|
||||
collection = do_get_kinto_collection(connection, COLLECTION_NAME);
|
||||
list = yield collection.list();
|
||||
do_check_eq(list.data.length, 4);
|
||||
yield connection.close();
|
||||
|
||||
// check that a pin exists for two.example.com and three.example.com
|
||||
ok(sss.isSecureHost(sss.HEADER_HPKP, "two.example.com", 0));
|
||||
ok(sss.isSecureHost(sss.HEADER_HPKP, "three.example.com", 0));
|
||||
|
||||
// check that a pin does not exist for four.example.com - it's in the
|
||||
// collection but the version should not match
|
||||
ok(!sss.isSecureHost(sss.HEADER_HPKP, "four.example.com", 0));
|
||||
|
||||
// Try to maybeSync with the current lastModified value - no connection
|
||||
// should be attempted.
|
||||
// Clear the kinto base pref so any connections will cause a test failure
|
||||
Services.prefs.clearUserPref("services.settings.server");
|
||||
yield PinningPreloadClient.maybeSync(4000, Date.now());
|
||||
|
||||
// Try again with a lastModified value at some point in the past
|
||||
yield PinningPreloadClient.maybeSync(3000, Date.now());
|
||||
|
||||
// Check the pinning check time pref is modified, even if the collection
|
||||
// hasn't changed
|
||||
Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
|
||||
yield PinningPreloadClient.maybeSync(3000, Date.now());
|
||||
let newValue = Services.prefs.getIntPref("services.blocklist.pinning.checked");
|
||||
do_check_neq(newValue, 0);
|
||||
|
||||
// Check that a sync completes even when there's bad data in the
|
||||
// collection. This will throw on fail, so just calling maybeSync is an
|
||||
// acceptible test (the data below with last_modified of 300 is nonsense).
|
||||
Services.prefs.setCharPref("services.settings.server",
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
yield PinningPreloadClient.maybeSync(5000, Date.now());
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// Ensure that signature verification is disabled to prevent interference
|
||||
// with basic certificate sync tests
|
||||
Services.prefs.setBoolPref("services.blocklist.signing.enforced", false);
|
||||
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(() => { });
|
||||
});
|
||||
}
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
const appInfo = Cc["@mozilla.org/xre/app-info;1"]
|
||||
.getService(Ci.nsIXULAppInfo);
|
||||
|
||||
const responses = {
|
||||
"OPTIONS": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
|
||||
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": "null"
|
||||
},
|
||||
"GET:/v1/?": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
|
||||
},
|
||||
"GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"3000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"pinType": "KeyPin",
|
||||
"hostName": "one.example.com",
|
||||
"includeSubdomains": false,
|
||||
"expires": new Date().getTime() + 1000000,
|
||||
"pins" : ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
|
||||
"M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
|
||||
"versions" : [appInfo.version],
|
||||
"id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
|
||||
"last_modified":3000
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified&_since=3000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"4000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"pinType": "KeyPin",
|
||||
"hostName": "two.example.com",
|
||||
"includeSubdomains": false,
|
||||
"expires": new Date().getTime() + 1000000,
|
||||
"pins" : ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
|
||||
"M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
|
||||
"versions" : [appInfo.version],
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02c",
|
||||
"last_modified":4000
|
||||
},{
|
||||
"pinType": "KeyPin",
|
||||
"hostName": "three.example.com",
|
||||
"includeSubdomains": false,
|
||||
"expires": new Date().getTime() + 1000000,
|
||||
"pins" : ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
|
||||
"M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
|
||||
"versions" : [appInfo.version, "some other version that won't match"],
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02d",
|
||||
"last_modified":4000
|
||||
},{
|
||||
"pinType": "KeyPin",
|
||||
"hostName": "four.example.com",
|
||||
"includeSubdomains": false,
|
||||
"expires": new Date().getTime() + 1000000,
|
||||
"pins" : ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
|
||||
"M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
|
||||
"versions" : ["some version that won't match"],
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02e",
|
||||
"last_modified":4000
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified&_since=4000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"5000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"irrelevant":"this entry looks nothing whatsoever like a pin preload",
|
||||
"pinType": "KeyPin",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02f",
|
||||
"last_modified":5000
|
||||
},{
|
||||
"irrelevant":"this entry has data of the wrong type",
|
||||
"pinType": "KeyPin",
|
||||
"hostName": 3,
|
||||
"includeSubdomains": "nonsense",
|
||||
"expires": "more nonsense",
|
||||
"pins" : [1,2,3,4],
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc030",
|
||||
"last_modified":5000
|
||||
},{
|
||||
"irrelevant":"this entry is missing the actual pins",
|
||||
"pinType": "KeyPin",
|
||||
"hostName": "missingpins.example.com",
|
||||
"includeSubdomains": false,
|
||||
"expires": new Date().getTime() + 1000000,
|
||||
"versions" : [appInfo.version],
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc031",
|
||||
"last_modified":5000
|
||||
}]})
|
||||
}
|
||||
};
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
|
||||
}
|
@ -57,6 +57,7 @@ add_task(function* test_check_maybeSync(){
|
||||
// add a test kinto client that will respond to lastModified information
|
||||
// for a collection called 'test-collection'
|
||||
updater.addTestBlocklistClient("test-collection", {
|
||||
bucketName: "blocklists",
|
||||
maybeSync(lastModified, serverTime) {
|
||||
do_check_eq(lastModified, 1000);
|
||||
do_check_eq(serverTime, 2000);
|
||||
|
@ -12,6 +12,7 @@ support-files =
|
||||
[test_blocklist_certificates.js]
|
||||
[test_blocklist_clients.js]
|
||||
[test_blocklist_updater.js]
|
||||
[test_blocklist_pinning.js]
|
||||
|
||||
[test_kinto.js]
|
||||
[test_blocklist_signatures.js]
|
||||
|
@ -12,7 +12,7 @@ from .graph import Graph
|
||||
from .taskgraph import TaskGraph
|
||||
from .optimize import optimize_task_graph
|
||||
from .util.python_path import find_object
|
||||
from .util.verifydoc import verify_docs
|
||||
from .util.verify import verify_docs, verify_task_graph_symbol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -217,6 +217,7 @@ class TaskGraphGenerator(object):
|
||||
target_task_graph = TaskGraph(
|
||||
{l: all_tasks[l] for l in target_graph.nodes},
|
||||
target_graph)
|
||||
target_task_graph.for_each_task(verify_task_graph_symbol, scratch_pad={})
|
||||
yield 'target_task_graph', target_task_graph
|
||||
|
||||
logger.info("Generating optimized task graph")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user