mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-01 13:57:32 +00:00
Merge f-t to m-c, a=merge
This commit is contained in:
commit
2ef98b0156
browser
base/content/test/general
components
extensions
privatebrowsing
locales/en-US/chrome/browser
themes
dom/push
layout/tools/reftest
mobile/android
base
tests/browser/robocop/components
modules/libpref/init
testing/talos
toolkit
components
devtools
locales/en-US/chrome/passwordmgr
mozapps/extensions
internal
test
addons/webextension_1
xpcshell
@ -24,6 +24,19 @@ const PREF_ACCEPTED_POLICY_DATE = PREF_BRANCH + "dataSubmissionPolicyNotifiedTim
|
||||
|
||||
const TEST_POLICY_VERSION = 37;
|
||||
|
||||
function fakeShowPolicyTimeout(set, clear) {
|
||||
let reportingPolicy =
|
||||
Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).Policy;
|
||||
reportingPolicy.setShowInfobarTimeout = set;
|
||||
reportingPolicy.clearShowInfobarTimeout = clear;
|
||||
}
|
||||
|
||||
function sendSessionRestoredNotification() {
|
||||
let reportingPolicyImpl =
|
||||
Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).TelemetryReportingPolicyImpl;
|
||||
reportingPolicyImpl.observe(null, "sessionstore-windows-restored", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a tick.
|
||||
*/
|
||||
@ -56,6 +69,21 @@ function promiseWaitForNotificationClose(aNotification) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function triggerInfoBar(expectedTimeoutMs) {
|
||||
let showInfobarCallback = null;
|
||||
let timeoutMs = null;
|
||||
fakeShowPolicyTimeout((callback, timeout) => {
|
||||
showInfobarCallback = callback;
|
||||
timeoutMs = timeout;
|
||||
}, () => {});
|
||||
sendSessionRestoredNotification();
|
||||
Assert.ok(!!showInfobarCallback, "Must have a timer callback.");
|
||||
if (expectedTimeoutMs !== undefined) {
|
||||
Assert.equal(timeoutMs, expectedTimeoutMs, "Timeout should match");
|
||||
}
|
||||
showInfobarCallback();
|
||||
}
|
||||
|
||||
let checkInfobarButton = Task.async(function* (aNotification) {
|
||||
// Check that the button on the data choices infobar does the right thing.
|
||||
let buttons = aNotification.getElementsByTagName("button");
|
||||
@ -130,11 +158,11 @@ add_task(function* test_single_window(){
|
||||
"User not notified about datareporting policy.");
|
||||
|
||||
let alertShownPromise = promiseWaitForAlertActive(notificationBox);
|
||||
// This should be false and trigger the Infobar.
|
||||
Assert.ok(!TelemetryReportingPolicy.canUpload(),
|
||||
"User should not be allowed to upload and the infobar should be triggered.");
|
||||
"User should not be allowed to upload.");
|
||||
|
||||
// Wait for the infobar to be displayed.
|
||||
triggerInfoBar(10 * 1000);
|
||||
yield alertShownPromise;
|
||||
|
||||
Assert.equal(notificationBox.allNotifications.length, 1, "Notification Displayed.");
|
||||
@ -185,10 +213,11 @@ add_task(function* test_multiple_windows(){
|
||||
promiseWaitForAlertActive(notificationBoxes[1])
|
||||
];
|
||||
|
||||
// This should be false and trigger the Infobar.
|
||||
Assert.ok(!TelemetryReportingPolicy.canUpload(),
|
||||
"User should not be allowed to upload and the infobar should be triggered.");
|
||||
"User should not be allowed to upload.");
|
||||
|
||||
// Wait for the infobars.
|
||||
triggerInfoBar(10 * 1000);
|
||||
yield Promise.all(showAlertPromises);
|
||||
|
||||
// Both notification were displayed. Close one and check that both gets closed.
|
||||
|
@ -5,10 +5,13 @@ const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
|
||||
|
||||
// Must run first.
|
||||
add_task(function* prepare() {
|
||||
// The test makes only sense if unified complete is enabled.
|
||||
Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
|
||||
let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
|
||||
let oldCurrentEngine = Services.search.currentEngine;
|
||||
Services.search.currentEngine = engine;
|
||||
registerCleanupFunction(function () {
|
||||
Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
|
||||
Services.search.currentEngine = oldCurrentEngine;
|
||||
Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
|
||||
Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
|
||||
|
@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env 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/.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import uuid
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
parser = argparse.ArgumentParser(description='Create install.rdf from manifest.json')
|
||||
parser.add_argument('--locale')
|
||||
parser.add_argument('--profile')
|
||||
parser.add_argument('--uuid')
|
||||
parser.add_argument('dir')
|
||||
args = parser.parse_args()
|
||||
|
||||
manifestFile = os.path.join(args.dir, 'manifest.json')
|
||||
manifest = json.load(open(manifestFile))
|
||||
|
||||
locale = args.locale
|
||||
if not locale:
|
||||
locale = manifest.get('default_locale', 'en-US')
|
||||
|
||||
def process_locale(s):
|
||||
if s.startswith('__MSG_') and s.endswith('__'):
|
||||
tag = s[6:-2]
|
||||
path = os.path.join(args.dir, '_locales', locale, 'messages.json')
|
||||
data = json.load(open(path))
|
||||
return data[tag]['message']
|
||||
else:
|
||||
return s
|
||||
|
||||
id = args.uuid
|
||||
if not id:
|
||||
id = '{' + str(uuid.uuid4()) + '}'
|
||||
|
||||
name = process_locale(manifest['name'])
|
||||
desc = process_locale(manifest['description'])
|
||||
version = manifest['version']
|
||||
|
||||
installFile = open(os.path.join(args.dir, 'install.rdf'), 'w')
|
||||
print >>installFile, '<?xml version="1.0"?>'
|
||||
print >>installFile, '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"'
|
||||
print >>installFile, ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">'
|
||||
print >>installFile
|
||||
print >>installFile, ' <Description about="urn:mozilla:install-manifest">'
|
||||
print >>installFile, ' <em:id>{}</em:id>'.format(id)
|
||||
print >>installFile, ' <em:type>2</em:type>'
|
||||
print >>installFile, ' <em:name>{}</em:name>'.format(name)
|
||||
print >>installFile, ' <em:description>{}</em:description>'.format(desc)
|
||||
print >>installFile, ' <em:version>{}</em:version>'.format(version)
|
||||
print >>installFile, ' <em:bootstrap>true</em:bootstrap>'
|
||||
|
||||
print >>installFile, ' <em:targetApplication>'
|
||||
print >>installFile, ' <Description>'
|
||||
print >>installFile, ' <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>'
|
||||
print >>installFile, ' <em:minVersion>4.0</em:minVersion>'
|
||||
print >>installFile, ' <em:maxVersion>50.0</em:maxVersion>'
|
||||
print >>installFile, ' </Description>'
|
||||
print >>installFile, ' </em:targetApplication>'
|
||||
|
||||
print >>installFile, ' </Description>'
|
||||
print >>installFile, '</RDF>'
|
||||
installFile.close()
|
||||
|
||||
bootstrapPath = os.path.join(os.path.dirname(sys.argv[0]), 'bootstrap.js')
|
||||
data = open(bootstrapPath).read()
|
||||
boot = open(os.path.join(args.dir, 'bootstrap.js'), 'w')
|
||||
boot.write(data)
|
||||
boot.close()
|
||||
|
||||
if args.profile:
|
||||
os.system('mkdir -p {}/extensions'.format(args.profile))
|
||||
output = open(args.profile + '/extensions/' + id, 'w')
|
||||
print >>output, os.path.realpath(args.dir)
|
||||
output.close()
|
||||
else:
|
||||
dir = os.path.realpath(args.dir)
|
||||
if dir[-1] == os.sep:
|
||||
dir = dir[:-1]
|
||||
os.system('cd "{}"; zip ../"{}".xpi -r *'.format(args.dir, os.path.basename(dir)))
|
@ -60,18 +60,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// Update state that depends on preferences.
|
||||
prefObserver.observe();
|
||||
|
||||
// This check can be removed when Tracking Protection is always available.
|
||||
let tpUIEnabled = false;
|
||||
try {
|
||||
tpUIEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.ui.enabled");
|
||||
} catch (ex) {
|
||||
// The preference is not available.
|
||||
}
|
||||
if (!tpUIEnabled) {
|
||||
document.getElementById("trackingProtectionSection")
|
||||
.setAttribute("hidden", "true");
|
||||
}
|
||||
}, false);
|
||||
|
||||
function openPrivateWindow() {
|
||||
|
@ -42,7 +42,7 @@
|
||||
<div class="list-header">&aboutPrivateBrowsing.info.forgotten;</div>
|
||||
<ul id="forgotten">
|
||||
<li>&aboutPrivateBrowsing.info.history;</li>
|
||||
<li>&aboutPrivateBrowsing.info.search;</li>
|
||||
<li>&aboutPrivateBrowsing.info.searches;</li>
|
||||
<li>&aboutPrivateBrowsing.info.cookies;</li>
|
||||
<li>&aboutPrivateBrowsing.info.temporaryFiles;</li>
|
||||
</ul>
|
||||
@ -55,7 +55,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p>&aboutPrivateBrowsing.note;</p>
|
||||
<p>&aboutPrivateBrowsing.note1;</p>
|
||||
<a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore;</a>
|
||||
</div>
|
||||
<div id="trackingProtectionSection"
|
||||
@ -69,7 +69,7 @@
|
||||
class="showTpDisabled">&trackingProtection.state.disabled;</span>
|
||||
</div>
|
||||
<p id="tpDiagram"/>
|
||||
<p>&trackingProtection.description;</p>
|
||||
<p>&trackingProtection.description1;</p>
|
||||
<!-- Use text links to implement plain styled buttons without an href. -->
|
||||
<label xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="disableTrackingProtection"
|
||||
@ -80,7 +80,7 @@
|
||||
class="text-link showTpDisabled"
|
||||
value="&trackingProtection.enable;"/>
|
||||
<p id="tpStartTour"
|
||||
class="showTpEnabled"><a id="startTour">&trackingProtection.startTour;</a></p>
|
||||
class="showTpEnabled"><a id="startTour">&trackingProtection.startTour1;</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -17,6 +17,7 @@ support-files =
|
||||
|
||||
[browser_privatebrowsing_DownloadLastDirWithCPS.js]
|
||||
[browser_privatebrowsing_about.js]
|
||||
tags = trackingprotection
|
||||
[browser_privatebrowsing_aboutHomeButtonAfterWindowClose.js]
|
||||
[browser_privatebrowsing_aboutSessionRestore.js]
|
||||
[browser_privatebrowsing_cache.js]
|
||||
|
@ -46,14 +46,12 @@ function* testLinkOpensUrl({ win, tab, elementId, expectedUrl }) {
|
||||
*/
|
||||
add_task(function* test_links() {
|
||||
// Use full version and change the remote URLs to prevent network access.
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.ui.enabled", true);
|
||||
Services.prefs.setCharPref("app.support.baseURL", "https://example.com/");
|
||||
Services.prefs.setCharPref("privacy.trackingprotection.introURL",
|
||||
"https://example.com/tour");
|
||||
registerCleanupFunction(function () {
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.introURL");
|
||||
Services.prefs.clearUserPref("app.support.baseURL");
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.ui.enabled");
|
||||
});
|
||||
|
||||
let { win, tab } = yield openAboutPrivateBrowsing();
|
||||
@ -77,12 +75,10 @@ add_task(function* test_links() {
|
||||
*/
|
||||
add_task(function* test_toggleTrackingProtection() {
|
||||
// Use tour version but disable Tracking Protection.
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.ui.enabled", true);
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled",
|
||||
true);
|
||||
registerCleanupFunction(function () {
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.pbmode.enabled");
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.ui.enabled");
|
||||
});
|
||||
|
||||
let { win, tab } = yield openAboutPrivateBrowsing();
|
||||
|
@ -12,13 +12,19 @@
|
||||
Width of the Private Browsing section.
|
||||
-->
|
||||
<!ENTITY aboutPrivateBrowsing.width "25em">
|
||||
|
||||
<!-- LOCALIZATION NOTE (aboutPrivateBrowsing.subtitle,
|
||||
aboutPrivateBrowsing.info.forgotten, aboutPrivateBrowsing.info.kept):
|
||||
These strings will be replaced by aboutPrivateBrowsing.forgotten and
|
||||
aboutPrivateBrowsing.kept when the new visual design lands (bug 1192625).
|
||||
-->
|
||||
<!ENTITY aboutPrivateBrowsing.title "You're browsing privately">
|
||||
<!ENTITY aboutPrivateBrowsing.subtitle "In this window, &brandShortName; will not remember any history.">
|
||||
|
||||
<!ENTITY aboutPrivateBrowsing.forgotten "In this window, &brandShortName; will not remember:">
|
||||
<!ENTITY aboutPrivateBrowsing.info.forgotten "Forgotten">
|
||||
<!ENTITY aboutPrivateBrowsing.info.history "History">
|
||||
<!ENTITY aboutPrivateBrowsing.info.search "Searches">
|
||||
<!ENTITY aboutPrivateBrowsing.info.searches "Searches">
|
||||
<!ENTITY aboutPrivateBrowsing.info.cookies "Cookies">
|
||||
<!ENTITY aboutPrivateBrowsing.info.temporaryFiles "Temporary Files">
|
||||
|
||||
@ -27,7 +33,7 @@
|
||||
<!ENTITY aboutPrivateBrowsing.info.downloads "Downloads">
|
||||
<!ENTITY aboutPrivateBrowsing.info.bookmarks "Bookmarks">
|
||||
|
||||
<!ENTITY aboutPrivateBrowsing.note "Please note that your employer or Internet service provider can still track the pages you visit.">
|
||||
<!ENTITY aboutPrivateBrowsing.note1 "Please note that your employer or Internet service provider can still track the pages you visit.">
|
||||
<!ENTITY aboutPrivateBrowsing.learnMore "Learn More.">
|
||||
|
||||
<!-- LOCALIZATION NOTE (trackingProtection.width):
|
||||
@ -44,8 +50,8 @@
|
||||
<!ENTITY trackingProtection.state.enabled "ON">
|
||||
<!ENTITY trackingProtection.state.disabled "OFF">
|
||||
|
||||
<!ENTITY trackingProtection.description "Private Windows now block parts of the page that may track your browsing activity.">
|
||||
<!ENTITY trackingProtection.description1 "Private Windows now block parts of the page that may track your browsing activity.">
|
||||
|
||||
<!ENTITY trackingProtection.disable "Turn Tracking Protection Off">
|
||||
<!ENTITY trackingProtection.enable "Turn Tracking Protection On">
|
||||
<!ENTITY trackingProtection.startTour "See how this works">
|
||||
<!ENTITY trackingProtection.startTour1 "See how this works">
|
||||
|
@ -87,7 +87,8 @@
|
||||
#TabsToolbar:not([collapsed="true"]) + #nav-bar {
|
||||
border-top: 1px solid hsla(0,0%,0%,.3) !important;
|
||||
background-clip: padding-box;
|
||||
margin-top: -1px; /* Move up into the TabsToolbar for the inner highlight at the top of the nav-bar */
|
||||
/* Move up into the TabsToolbar for the inner highlight at the top of the nav-bar */
|
||||
margin-top: calc(-1 * var(--navbar-tab-toolbar-highlight-overlap));
|
||||
/* Position the toolbar above the bottom of background tabs */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -1568,7 +1569,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1 {
|
||||
margin-bottom: var(--tab-toolbar-navbar-overlap);
|
||||
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
|
||||
}
|
||||
|
||||
#alltabs-button {
|
||||
|
@ -207,7 +207,7 @@ toolbarseparator {
|
||||
#TabsToolbar:not([collapsed="true"]) + #nav-bar:-moz-lwtheme {
|
||||
border-top: 1px solid hsla(0,0%,0%,.3);
|
||||
background-clip: padding-box;
|
||||
margin-top: calc(-1 * var(--tab-toolbar-navbar-overlap));
|
||||
margin-top: calc(-1 * var(--navbar-tab-toolbar-highlight-overlap));
|
||||
/* Position the toolbar above the bottom of background tabs */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -219,7 +219,7 @@ toolbarseparator {
|
||||
#main-window[tabsintitlebar] #TabsToolbar:not([collapsed="true"]) + #nav-bar:not(:-moz-lwtheme) {
|
||||
border-top: 1px solid hsla(0,0%,0%,.2);
|
||||
background-clip: padding-box;
|
||||
margin-top: calc(-1 * var(--tab-toolbar-navbar-overlap));
|
||||
margin-top: calc(-1 * var(--navbar-tab-toolbar-highlight-overlap));
|
||||
/* Position the toolbar above the bottom of background tabs */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -2847,7 +2847,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1 {
|
||||
margin-bottom: var(--tab-toolbar-navbar-overlap);
|
||||
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
:root {
|
||||
--tab-toolbar-navbar-overlap: 0px;
|
||||
--navbar-tab-toolbar-highlight-overlap: 0px;
|
||||
--space-above-tabbar: 0px;
|
||||
--toolbarbutton-text-shadow: none;
|
||||
--backbutton-urlbar-overlap: 0px;
|
||||
@ -310,11 +311,6 @@ searchbar:not([oneoffui]) .search-go-button {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tabbrowser-tab {
|
||||
/* We normally rely on other tab elements for pointer events, but this
|
||||
theme hides those so we need it set here instead */
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
:root {
|
||||
--tab-toolbar-navbar-overlap: 1px;
|
||||
--navbar-tab-toolbar-highlight-overlap: 1px;
|
||||
--tab-min-height: 31px;
|
||||
}
|
||||
#TabsToolbar {
|
||||
@ -263,7 +264,7 @@
|
||||
background-image: url(chrome://browser/skin/tabbrowser/tab-overflow-indicator.png);
|
||||
background-size: 100% 100%;
|
||||
width: 14px;
|
||||
margin-bottom: var(--tab-toolbar-navbar-overlap);
|
||||
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
z-index: 3; /* the selected tab's z-index + 1 */
|
||||
|
@ -308,7 +308,8 @@
|
||||
}
|
||||
|
||||
#TabsToolbar:not([collapsed="true"]) + #nav-bar {
|
||||
margin-top: -1px; /* Move up into the TabsToolbar for the inner highlight at the top of the nav-bar */
|
||||
/* Move up into the TabsToolbar for the inner highlight at the top of the nav-bar */
|
||||
margin-top: calc(-1 * var(--navbar-tab-toolbar-highlight-overlap));
|
||||
/* Position the toolbar above the bottom of background tabs */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -928,7 +929,7 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1 {
|
||||
margin-bottom: var(--tab-toolbar-navbar-overlap);
|
||||
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1:not([disabled=true]):hover,
|
||||
|
@ -6,7 +6,7 @@
|
||||
"use strict";
|
||||
|
||||
// Don't modify this, instead set dom.push.debug.
|
||||
let gDebuggingEnabled = true;
|
||||
let gDebuggingEnabled = false;
|
||||
|
||||
function debug(s) {
|
||||
if (gDebuggingEnabled) {
|
||||
|
@ -213,6 +213,9 @@ class RefTest(object):
|
||||
# Likewise for safebrowsing.
|
||||
prefs['browser.safebrowsing.enabled'] = False
|
||||
prefs['browser.safebrowsing.malware.enabled'] = False
|
||||
# Likewise for tracking protection.
|
||||
prefs['privacy.trackingprotection.enabled'] = False
|
||||
prefs['privacy.trackingprotection.pbmode.enabled'] = False
|
||||
# And for snippets.
|
||||
prefs['browser.snippets.enabled'] = False
|
||||
prefs['browser.snippets.syncPromo.enabled'] = False
|
||||
|
@ -3969,6 +3969,9 @@ public class BrowserApp extends GeckoApp
|
||||
if (inGuestMode) {
|
||||
return StartupAction.GUEST;
|
||||
}
|
||||
if (RestrictedProfiles.isRestrictedProfile(this)) {
|
||||
return StartupAction.RESTRICTED;
|
||||
}
|
||||
return (passedURL == null ? StartupAction.NORMAL : StartupAction.URL);
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,8 @@ public abstract class GeckoApp
|
||||
URL, /* launched with a passed URL */
|
||||
PREFETCH, /* launched with a passed URL that we prefetch */
|
||||
WEBAPP, /* launched as a webapp runtime */
|
||||
GUEST /* launched in guest browsing */
|
||||
GUEST, /* launched in guest browsing */
|
||||
RESTRICTED /* launched with restricted profile */
|
||||
}
|
||||
|
||||
public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
|
||||
|
@ -74,24 +74,15 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
|
||||
EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
|
||||
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.READING_LIST));
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.HISTORY));
|
||||
|
||||
final PanelConfig historyEntry = createBuiltinPanelConfig(mContext, PanelType.HISTORY);
|
||||
final PanelConfig recentTabsEntry = createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS);
|
||||
|
||||
// We disable Synced Tabs for guest mode profiles.
|
||||
final PanelConfig remoteTabsEntry;
|
||||
// We disable Synced Tabs for guest mode / restricted profiles.
|
||||
if (RestrictedProfiles.isAllowed(mContext, Restriction.DISALLOW_MODIFY_ACCOUNTS)) {
|
||||
remoteTabsEntry = createBuiltinPanelConfig(mContext, PanelType.REMOTE_TABS);
|
||||
} else {
|
||||
remoteTabsEntry = null;
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.REMOTE_TABS));
|
||||
}
|
||||
|
||||
panelConfigs.add(historyEntry);
|
||||
panelConfigs.add(recentTabsEntry);
|
||||
if (remoteTabsEntry != null) {
|
||||
panelConfigs.add(remoteTabsEntry);
|
||||
}
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS));
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.READING_LIST));
|
||||
|
||||
return new State(panelConfigs, true);
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ public class AboutHomeComponent extends BaseComponent {
|
||||
private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
|
||||
PanelType.TOP_SITES,
|
||||
PanelType.BOOKMARKS,
|
||||
PanelType.READING_LIST,
|
||||
PanelType.HISTORY,
|
||||
PanelType.REMOTE_TABS,
|
||||
PanelType.RECENT_TABS,
|
||||
PanelType.REMOTE_TABS
|
||||
PanelType.READING_LIST
|
||||
);
|
||||
|
||||
// The percentage of the panel to swipe between 0 and 1. This value was set through
|
||||
|
@ -1087,7 +1087,7 @@ pref("privacy.donottrackheader.enabled", false);
|
||||
// Enforce tracking protection in all modes
|
||||
pref("privacy.trackingprotection.enabled", false);
|
||||
// Enforce tracking protection in Private Browsing mode
|
||||
pref("privacy.trackingprotection.pbmode.enabled", false);
|
||||
pref("privacy.trackingprotection.pbmode.enabled", true);
|
||||
|
||||
pref("dom.event.contextmenu.enabled", true);
|
||||
pref("dom.event.clipboardevents.enabled", true);
|
||||
|
@ -5,7 +5,7 @@
|
||||
},
|
||||
"global": {
|
||||
"talos_repo": "https://hg.mozilla.org/build/talos",
|
||||
"talos_revision": "d44548b8feb9"
|
||||
"talos_revision": "c7446ecc3bfb"
|
||||
},
|
||||
"extra_options": {
|
||||
"android": [ "--apkPath=%(apk_path)s" ]
|
||||
|
@ -141,6 +141,10 @@ let Management = {
|
||||
this.lazyInit();
|
||||
this.emitter.emit(hook, ...args);
|
||||
},
|
||||
|
||||
off(hook, callback) {
|
||||
this.emitter.off(hook, callback);
|
||||
}
|
||||
};
|
||||
|
||||
// A MessageBroker that's used to send and receive messages for
|
||||
@ -529,13 +533,13 @@ Extension.prototype = {
|
||||
},
|
||||
|
||||
startup() {
|
||||
GlobalManager.init(this);
|
||||
|
||||
return Promise.all([this.readManifest(), this.readLocaleMessages()]).then(([manifest, messages]) => {
|
||||
if (this.hasShutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalManager.init(this);
|
||||
|
||||
this.manifest = manifest;
|
||||
this.localeMessages = messages;
|
||||
|
||||
|
@ -337,14 +337,10 @@ let TelemetryReportingPolicyImpl = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the user is notified of the current policy. If he isn't, don't try
|
||||
// to upload anything.
|
||||
if (!this._ensureUserNotified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Submission is enabled and user is notified: upload is allowed.
|
||||
return true;
|
||||
// Submission is enabled. We enable upload if user is notified or we need to bypass
|
||||
// the policy.
|
||||
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
|
||||
return this.isUserNotifiedOfCurrentPolicy || bypassNotification;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -358,26 +354,30 @@ let TelemetryReportingPolicyImpl = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Make sure the user is notified about the policy before allowing upload.
|
||||
* @return {Boolean} True if the user was notified, false otherwise.
|
||||
* Show the data choices infobar if the user wasn't already notified and data submission
|
||||
* is enabled.
|
||||
*/
|
||||
_ensureUserNotified: function() {
|
||||
const BYPASS_NOTIFICATION = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
|
||||
if (this.isUserNotifiedOfCurrentPolicy || BYPASS_NOTIFICATION) {
|
||||
return true;
|
||||
_showInfobar: function() {
|
||||
if (!this.dataSubmissionEnabled) {
|
||||
this._log.trace("_showInfobar - Data submission disabled by the policy.");
|
||||
return;
|
||||
}
|
||||
|
||||
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
|
||||
if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) {
|
||||
this._log.trace("_showInfobar - User already notified or bypassing the policy.");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.trace("ensureUserNotified - User not notified, notifying now.");
|
||||
if (this._notificationInProgress) {
|
||||
this._log.trace("ensureUserNotified - User not notified, notification in progress.");
|
||||
return false;
|
||||
this._log.trace("_showInfobar - User not notified, notification already in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.trace("_showInfobar - User not notified, notifying now.");
|
||||
this._notificationInProgress = true;
|
||||
let request = new NotifyPolicyRequest(this._log);
|
||||
Observers.notify("datareporting:notify-data-policy:request", request);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -412,7 +412,7 @@ let TelemetryReportingPolicyImpl = {
|
||||
|
||||
this._startupNotificationTimerId = Policy.setShowInfobarTimeout(
|
||||
// Calling |canUpload| eventually shows the infobar, if needed.
|
||||
() => this.canUpload(), delay);
|
||||
() => this._showInfobar(), delay);
|
||||
// We performed at least a run, flip the firstRun preference.
|
||||
Preferences.set(PREF_FIRST_RUN, false);
|
||||
},
|
||||
|
@ -113,6 +113,20 @@ function isDeletionPing(aPing) {
|
||||
return isV4PingFormat(aPing) && (aPing.type == PING_TYPE_DELETION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the provided ping as a pending ping. If it's a deletion ping, save it
|
||||
* to a special location.
|
||||
* @param {Object} aPing The ping to save.
|
||||
* @return {Promise} A promise resolved when the ping is saved.
|
||||
*/
|
||||
function savePing(aPing) {
|
||||
if (isDeletionPing(aPing)) {
|
||||
return TelemetryStorage.saveDeletionPing(aPing);
|
||||
} else {
|
||||
return TelemetryStorage.savePendingPing(aPing);
|
||||
}
|
||||
}
|
||||
|
||||
function tomorrow(date) {
|
||||
let d = new Date(date);
|
||||
d.setDate(d.getDate() + 1);
|
||||
@ -673,7 +687,7 @@ let TelemetrySendImpl = {
|
||||
// Sending is disabled or throttled, add this to the persisted pending pings.
|
||||
this._log.trace("submitPing - can't send ping now, persisting to disk - " +
|
||||
"canSendNow: " + this.canSendNow);
|
||||
return TelemetryStorage.savePendingPing(ping);
|
||||
return savePing(ping);
|
||||
}
|
||||
|
||||
// Let the scheduler trigger sending pings if possible.
|
||||
@ -716,11 +730,7 @@ let TelemetrySendImpl = {
|
||||
} catch (ex) {
|
||||
this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
|
||||
// Deletion pings must be saved to a special location.
|
||||
if (isDeletionPing(ping)) {
|
||||
yield TelemetryStorage.saveDeletionPing(ping);
|
||||
} else {
|
||||
yield TelemetryStorage.savePendingPing(ping);
|
||||
}
|
||||
yield savePing(ping);
|
||||
} finally {
|
||||
this._currentPings.delete(ping.id);
|
||||
}
|
||||
@ -1022,7 +1032,7 @@ let TelemetrySendImpl = {
|
||||
_persistCurrentPings: Task.async(function*() {
|
||||
for (let [id, ping] of this._currentPings) {
|
||||
try {
|
||||
yield TelemetryStorage.savePendingPing(ping);
|
||||
yield savePing(ping);
|
||||
this._log.trace("_persistCurrentPings - saved ping " + id);
|
||||
} catch (ex) {
|
||||
this._log.error("_persistCurrentPings - failed to save ping " + id, ex);
|
||||
|
@ -47,6 +47,8 @@ skip-if = buildapp == 'mulet'
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_framerate_05.html]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_framerate_06.html]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_getProcess.html]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_inspector-anonymous.html]
|
||||
|
@ -0,0 +1,82 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1171489 - Tests if the framerate actor does not record timestamps from multiple frames.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Framerate actor test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script>
|
||||
|
||||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
var {FramerateFront} = require("devtools/server/actors/framerate");
|
||||
var {TargetFactory} = require("devtools/framework/target");
|
||||
|
||||
var url = document.getElementById("testContent").href;
|
||||
attachURL(url, onTab);
|
||||
|
||||
function onTab(_, client, form, contentDoc) {
|
||||
var contentWin = contentDoc.defaultView;
|
||||
var chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
var selectedTab = chromeWin.gBrowser.selectedTab;
|
||||
|
||||
var target = TargetFactory.forTab(selectedTab);
|
||||
var front = FramerateFront(client, form);
|
||||
|
||||
front.startRecording().then(() => {
|
||||
window.setTimeout(() => {
|
||||
// Wait for the iframe to be loaded again
|
||||
window.addEventListener("message", function loaded (event) {
|
||||
if (event.data === "ready") {
|
||||
window.removeEventListener("message", loaded);
|
||||
window.setTimeout(() => {
|
||||
front.stopRecording().then(ticks => {
|
||||
onRecordingStopped(client, ticks);
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
contentWin.location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function onRecordingStopped(client, ticks) {
|
||||
var diffs = [];
|
||||
|
||||
info(`Got ${ticks.length} ticks.`);
|
||||
|
||||
for (var i = 1; i < ticks.length; i++) {
|
||||
var prev = ticks[i - 1];
|
||||
var curr = ticks[i];
|
||||
diffs.push(curr - prev);
|
||||
info(curr + " - " + (curr - prev));
|
||||
}
|
||||
|
||||
// 1000 / 60 => 16.666... so we shouldn't get more than diffs of 16.66.. but
|
||||
// when we get ticks from other frames they're usually at diffs of < 1. Sometimes
|
||||
// ticks can still be less than 16ms even on one frame (usually following a very slow
|
||||
// frame), so use a low number (2) to be our threshold
|
||||
var THRESHOLD = 2;
|
||||
ok(ticks.length >= 60, "we should have 2 seconds worth of ticks, atleast 60 ticks");
|
||||
var belowThreshold = diffs.filter(v => v <= THRESHOLD);
|
||||
ok(belowThreshold.length <= 10, "we should have very few frames less than the threshold");
|
||||
|
||||
client.close(() => {
|
||||
DebuggerServer.destroy();
|
||||
SimpleTest.finish()
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</pre>
|
||||
<a id="testContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
|
||||
</body>
|
||||
</html>
|
@ -92,6 +92,7 @@ let Framerate = exports.Framerate = Class({
|
||||
*/
|
||||
_onGlobalCreated: function (win) {
|
||||
if (this._recording) {
|
||||
this._contentWin.cancelAnimationFrame(this._rafID);
|
||||
this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,9 @@
|
||||
<!ENTITY removeall.label "Remove All">
|
||||
<!ENTITY removeall.accesskey "A">
|
||||
|
||||
<!ENTITY addLogin.label "Add Login">
|
||||
<!ENTITY addLogin.accesskey "L">
|
||||
|
||||
<!ENTITY import.label "Import…">
|
||||
<!ENTITY import.accesskey "I">
|
||||
|
||||
|
@ -54,6 +54,8 @@ showPasswordsAccessKey=P
|
||||
noMasterPasswordPrompt=Are you sure you wish to show your passwords?
|
||||
removeAllPasswordsPrompt=Are you sure you wish to remove all passwords?
|
||||
removeAllPasswordsTitle=Remove all passwords
|
||||
removeLoginPrompt=Are you sure you wish to remove this login?
|
||||
removeLoginTitle=Remove login
|
||||
loginsSpielAll=Passwords for the following sites are stored on your computer:
|
||||
loginsSpielFiltered=The following passwords match your search:
|
||||
# LOCALIZATION NOTE (loginHostAge):
|
||||
@ -63,3 +65,5 @@ loginHostAge=%1$S (%2$S)
|
||||
# LOCALIZATION NOTE (noUsername):
|
||||
# String is used on the context menu when a login doesn't have a username.
|
||||
noUsername=No username
|
||||
duplicateLoginTitle=Login already exists
|
||||
duplicateLogin=A duplicate login already exists.
|
||||
|
@ -8,6 +8,10 @@ Components.utils.import("resource://gre/modules/Extension.jsm");
|
||||
|
||||
let extension;
|
||||
|
||||
function install(data, reason)
|
||||
{
|
||||
}
|
||||
|
||||
function startup(data, reason)
|
||||
{
|
||||
extension = new Extension(data);
|
||||
@ -18,3 +22,7 @@ function shutdown(data, reason)
|
||||
{
|
||||
extension.shutdown();
|
||||
}
|
||||
|
||||
function uninstall(data, reason)
|
||||
{
|
||||
}
|
@ -121,7 +121,8 @@ const DIR_TRASH = "trash";
|
||||
|
||||
const FILE_DATABASE = "extensions.json";
|
||||
const FILE_OLD_CACHE = "extensions.cache";
|
||||
const FILE_INSTALL_MANIFEST = "install.rdf";
|
||||
const FILE_RDF_MANIFEST = "install.rdf";
|
||||
const FILE_WEB_MANIFEST = "manifest.json";
|
||||
const FILE_XPI_ADDONS_LIST = "extensions.ini";
|
||||
|
||||
const KEY_PROFILEDIR = "ProfD";
|
||||
@ -202,13 +203,21 @@ const TYPES = {
|
||||
experiment: 128,
|
||||
};
|
||||
|
||||
// Some add-on types that we track internally are presented as other types
|
||||
// externally
|
||||
const TYPE_ALIASES = {
|
||||
"webextension": "extension",
|
||||
};
|
||||
|
||||
const RESTARTLESS_TYPES = new Set([
|
||||
"webextension",
|
||||
"dictionary",
|
||||
"experiment",
|
||||
"locale",
|
||||
]);
|
||||
|
||||
const SIGNED_TYPES = new Set([
|
||||
"webextension",
|
||||
"extension",
|
||||
"experiment",
|
||||
]);
|
||||
@ -641,6 +650,65 @@ function createAddonDetails(id, aAddon) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an internal add-on type to the type presented through the API.
|
||||
*
|
||||
* @param aType
|
||||
* The internal add-on type
|
||||
* @return an external add-on type
|
||||
*/
|
||||
function getExternalType(aType) {
|
||||
if (aType in TYPE_ALIASES)
|
||||
return TYPE_ALIASES[aType];
|
||||
return aType;
|
||||
}
|
||||
|
||||
function getManifestFileForDir(aDir) {
|
||||
let file = aDir.clone();
|
||||
file.append(FILE_WEB_MANIFEST);
|
||||
if (file.exists() && file.isFile())
|
||||
return file;
|
||||
file.leafName = FILE_RDF_MANIFEST;
|
||||
if (file.exists() && file.isFile())
|
||||
return file;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getManifestEntryForZipReader(aZipReader) {
|
||||
if (aZipReader.hasEntry(FILE_WEB_MANIFEST))
|
||||
return FILE_WEB_MANIFEST;
|
||||
if (aZipReader.hasEntry(FILE_RDF_MANIFEST))
|
||||
return FILE_RDF_MANIFEST;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of API types to a list of API types and any aliases for those
|
||||
* types.
|
||||
*
|
||||
* @param aTypes
|
||||
* An array of types or null for all types
|
||||
* @return an array of types or null for all types
|
||||
*/
|
||||
function getAllAliasesForTypes(aTypes) {
|
||||
if (!aTypes)
|
||||
return null;
|
||||
|
||||
// Build a set of all requested types and their aliases
|
||||
let typeset = new Set(aTypes);
|
||||
|
||||
for (let alias of Object.keys(TYPE_ALIASES)) {
|
||||
// Ignore any requested internal types
|
||||
typeset.delete(alias);
|
||||
|
||||
// Add any alias for the internal type
|
||||
if (typeset.has(TYPE_ALIASES[alias]))
|
||||
typeset.add(alias);
|
||||
}
|
||||
|
||||
return [...typeset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RDF literal, resource or integer into a string.
|
||||
*
|
||||
@ -673,6 +741,96 @@ function getRDFProperty(aDs, aResource, aProperty) {
|
||||
return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an AddonInternal object from a manifest stream.
|
||||
*
|
||||
* @param aStream
|
||||
* An open stream to read the manifest from
|
||||
* @return an AddonInternal object
|
||||
* @throws if the install manifest in the stream is corrupt or could not
|
||||
* be read
|
||||
*/
|
||||
function loadManifestFromWebManifest(aStream) {
|
||||
let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
let manifest = decoder.decodeFromStream(aStream, aStream.available());
|
||||
|
||||
function findProp(obj, current, properties) {
|
||||
if (properties.length == 0)
|
||||
return obj;
|
||||
|
||||
let field = properties[0];
|
||||
current += "." + field;
|
||||
if (!obj || !(field in obj)) {
|
||||
throw new Error("Manifest file was missing required property " + current.substring(1));
|
||||
}
|
||||
|
||||
return findProp(obj[field], current, properties.slice(1));
|
||||
}
|
||||
|
||||
function getProp(path) {
|
||||
return findProp(manifest, "", path.split("."));
|
||||
}
|
||||
|
||||
function getOptionalProp(path, defValue = null) {
|
||||
try {
|
||||
return findProp(manifest, "", path.split("."));
|
||||
}
|
||||
catch (e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
let mVersion = getProp("manifest_version");
|
||||
if (mVersion != 2) {
|
||||
throw new Error("Expected manifest_version to be 2 but was " + mVersion);
|
||||
}
|
||||
|
||||
let addon = new AddonInternal();
|
||||
addon.id = getProp("applications.gecko.id");
|
||||
if (!gIDTest.test(addon.id))
|
||||
throw new Error("Illegal add-on ID " + addon.id);
|
||||
addon.version = getProp("version");
|
||||
addon.type = "webextension";
|
||||
addon.unpack = false;
|
||||
addon.strictCompatibility = true;
|
||||
addon.bootstrap = true;
|
||||
addon.hasBinaryComponents = false;
|
||||
addon.multiprocessCompatible = true;
|
||||
addon.internalName = null;
|
||||
addon.updateURL = null;
|
||||
addon.updateKey = null;
|
||||
addon.optionsURL = null;
|
||||
addon.optionsType = null;
|
||||
addon.aboutURL = null;
|
||||
addon.iconURL = null;
|
||||
addon.icon64URL = null;
|
||||
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
|
||||
|
||||
addon.defaultLocale = {
|
||||
name: getProp("name"),
|
||||
description: getOptionalProp("description"),
|
||||
creator: null,
|
||||
homepageURL: null,
|
||||
|
||||
developers: null,
|
||||
translators: null,
|
||||
contributors: null,
|
||||
}
|
||||
|
||||
addon.targetApplications = [{
|
||||
id: TOOLKIT_ID,
|
||||
minVersion: "42a1",
|
||||
maxVersion: "*",
|
||||
}];
|
||||
|
||||
addon.locales = [];
|
||||
addon.targetPlatforms = [];
|
||||
addon.userDisabled = false;
|
||||
addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
|
||||
|
||||
return addon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an AddonInternal object from an RDF stream.
|
||||
*
|
||||
@ -934,14 +1092,17 @@ function loadManifestFromRDF(aUri, aStream) {
|
||||
addon.targetPlatforms = [];
|
||||
}
|
||||
|
||||
return addon;
|
||||
}
|
||||
|
||||
function defineSyncGUID(aAddon) {
|
||||
// Load the storage service before NSS (nsIRandomGenerator),
|
||||
// to avoid a SQLite initialization error (bug 717904).
|
||||
let storage = Services.storage;
|
||||
|
||||
// Define .syncGUID as a lazy property which is also settable
|
||||
Object.defineProperty(addon, "syncGUID", {
|
||||
Object.defineProperty(aAddon, "syncGUID", {
|
||||
get: () => {
|
||||
|
||||
// Generate random GUID used for Sync.
|
||||
// This was lifted from util.js:makeGUID() from services-sync.
|
||||
let rng = Cc["@mozilla.org/security/random-generator;1"].
|
||||
@ -953,19 +1114,17 @@ function loadManifestFromRDF(aUri, aStream) {
|
||||
let guid = btoa(byte_string).replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
delete addon.syncGUID;
|
||||
addon.syncGUID = guid;
|
||||
delete aAddon.syncGUID;
|
||||
aAddon.syncGUID = guid;
|
||||
return guid;
|
||||
},
|
||||
set: (val) => {
|
||||
delete addon.syncGUID;
|
||||
addon.syncGUID = val;
|
||||
delete aAddon.syncGUID;
|
||||
aAddon.syncGUID = val;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
return addon;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -993,11 +1152,22 @@ let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir) {
|
||||
return size;
|
||||
}
|
||||
|
||||
let file = aDir.clone();
|
||||
file.append(FILE_INSTALL_MANIFEST);
|
||||
if (!file.exists() || !file.isFile())
|
||||
function loadFromRDF(aFile, aStream) {
|
||||
let addon = loadManifestFromRDF(Services.io.newFileURI(aFile), aStream);
|
||||
|
||||
let file = aDir.clone();
|
||||
file.append("chrome.manifest");
|
||||
let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
|
||||
addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
|
||||
"binary-component");
|
||||
return addon;
|
||||
}
|
||||
|
||||
let file = getManifestFileForDir(aDir);
|
||||
if (!file) {
|
||||
throw new Error("Directory " + aDir.path + " does not contain a valid " +
|
||||
"install manifest");
|
||||
}
|
||||
|
||||
let fis = Cc["@mozilla.org/network/file-input-stream;1"].
|
||||
createInstance(Ci.nsIFileInputStream);
|
||||
@ -1007,19 +1177,17 @@ let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir) {
|
||||
bis.init(fis, 4096);
|
||||
|
||||
try {
|
||||
let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
|
||||
let addon = file.leafName == FILE_WEB_MANIFEST ?
|
||||
loadManifestFromWebManifest(bis) :
|
||||
loadFromRDF(file, bis);
|
||||
|
||||
addon._sourceBundle = aDir.clone();
|
||||
addon.size = getFileSize(aDir);
|
||||
|
||||
file = aDir.clone();
|
||||
file.append("chrome.manifest");
|
||||
let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
|
||||
addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
|
||||
"binary-component");
|
||||
|
||||
addon.signedState = yield verifyDirSignedState(aDir, addon);
|
||||
|
||||
addon.appDisabled = !isUsableAddon(addon);
|
||||
|
||||
defineSyncGUID(addon);
|
||||
|
||||
return addon;
|
||||
}
|
||||
finally {
|
||||
@ -1037,20 +1205,9 @@ let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir) {
|
||||
* @throws if the XPI file does not contain a valid install manifest
|
||||
*/
|
||||
let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader) {
|
||||
let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
|
||||
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
|
||||
createInstance(Ci.nsIBufferedInputStream);
|
||||
bis.init(zis, 4096);
|
||||
|
||||
try {
|
||||
let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
|
||||
let addon = loadManifestFromRDF(uri, bis);
|
||||
addon._sourceBundle = aZipReader.file;
|
||||
|
||||
addon.size = 0;
|
||||
let entries = aZipReader.findEntries(null);
|
||||
while (entries.hasMore())
|
||||
addon.size += aZipReader.getEntry(entries.getNext()).realSize;
|
||||
function loadFromRDF(aStream) {
|
||||
let uri = buildJarURI(aZipReader.file, FILE_RDF_MANIFEST);
|
||||
let addon = loadManifestFromRDF(uri, aStream);
|
||||
|
||||
// Binary components can only be loaded from unpacked addons.
|
||||
if (addon.unpack) {
|
||||
@ -1062,9 +1219,37 @@ let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(a
|
||||
addon.hasBinaryComponents = false;
|
||||
}
|
||||
|
||||
addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
|
||||
return addon;
|
||||
}
|
||||
|
||||
let entry = getManifestEntryForZipReader(aZipReader);
|
||||
if (!entry) {
|
||||
throw new Error("File " + aZipReader.file.path + " does not contain a valid " +
|
||||
"install manifest");
|
||||
}
|
||||
|
||||
let zis = aZipReader.getInputStream(entry);
|
||||
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
|
||||
createInstance(Ci.nsIBufferedInputStream);
|
||||
bis.init(zis, 4096);
|
||||
|
||||
try {
|
||||
let addon = entry == FILE_WEB_MANIFEST ?
|
||||
loadManifestFromWebManifest(bis) :
|
||||
loadFromRDF(bis);
|
||||
|
||||
addon._sourceBundle = aZipReader.file;
|
||||
|
||||
addon.size = 0;
|
||||
let entries = aZipReader.findEntries(null);
|
||||
while (entries.hasMore())
|
||||
addon.size += aZipReader.getEntry(entries.getNext()).realSize;
|
||||
|
||||
addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
|
||||
addon.appDisabled = !isUsableAddon(addon);
|
||||
|
||||
defineSyncGUID(addon);
|
||||
|
||||
return addon;
|
||||
}
|
||||
finally {
|
||||
@ -1640,15 +1825,12 @@ XPIState.prototype = {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
// if the add-on is disabled, modified time is the install.rdf time, if any.
|
||||
// If {path}/install.rdf doesn't exist, we assume this is a packed .xpi and use
|
||||
// if the add-on is disabled, modified time is the install manifest time, if
|
||||
// any. If no manifest exists, we assume this is a packed .xpi and use
|
||||
// the time stamp of {path}
|
||||
try {
|
||||
// Get the install.rdf update time, if any.
|
||||
// XXX This will eventually also need to check for package.json or whatever
|
||||
// the new manifest is named.
|
||||
let maniFile = aFile.clone();
|
||||
maniFile.append(FILE_INSTALL_MANIFEST);
|
||||
// Get the install manifest update time, if any.
|
||||
let maniFile = getManifestFileForDir(aFile);
|
||||
if (!(aId in XPIProvider._mostRecentlyModifiedFile)) {
|
||||
XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName;
|
||||
}
|
||||
@ -2660,12 +2842,11 @@ this.XPIProvider = {
|
||||
|
||||
if (isDir) {
|
||||
// Check if the directory contains an install manifest.
|
||||
let manifest = stageDirEntry.clone();
|
||||
manifest.append(FILE_INSTALL_MANIFEST);
|
||||
let manifest = getManifestFileForDir(stageDirEntry);
|
||||
|
||||
// If the install manifest doesn't exist uninstall this add-on in this
|
||||
// install location.
|
||||
if (!manifest.exists()) {
|
||||
if (!manifest) {
|
||||
logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
|
||||
try {
|
||||
aLocation.uninstallAddon(id);
|
||||
@ -3959,7 +4140,9 @@ this.XPIProvider = {
|
||||
* A callback to pass an array of Addons to
|
||||
*/
|
||||
getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
|
||||
XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) {
|
||||
let typesToGet = getAllAliasesForTypes(aTypes);
|
||||
|
||||
XPIDatabase.getVisibleAddons(typesToGet, function getAddonsByTypes_getVisibleAddons(aAddons) {
|
||||
aCallback([createWrapper(a) for each (a in aAddons)]);
|
||||
});
|
||||
},
|
||||
@ -3988,7 +4171,9 @@ this.XPIProvider = {
|
||||
*/
|
||||
getAddonsWithOperationsByTypes:
|
||||
function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
|
||||
XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes,
|
||||
let typesToGet = getAllAliasesForTypes(aTypes);
|
||||
|
||||
XPIDatabase.getVisibleAddonsWithPendingOperations(typesToGet,
|
||||
function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) {
|
||||
let results = [createWrapper(a) for each (a in aAddons)];
|
||||
XPIProvider.installs.forEach(function(aInstall) {
|
||||
@ -4012,7 +4197,7 @@ this.XPIProvider = {
|
||||
getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
|
||||
let results = [];
|
||||
this.installs.forEach(function(aInstall) {
|
||||
if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
|
||||
if (!aTypes || aTypes.indexOf(getExternalType(aInstall.type)) >= 0)
|
||||
results.push(aInstall.wrapper);
|
||||
});
|
||||
aCallback(results);
|
||||
@ -4451,6 +4636,8 @@ this.XPIProvider = {
|
||||
let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
|
||||
if (aType == "dictionary")
|
||||
uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
|
||||
else if (aType == "webextension")
|
||||
uri = "resource://gre/modules/addons/WebExtensionBootstrap.js"
|
||||
|
||||
this.bootstrapScopes[aId] =
|
||||
new Cu.Sandbox(principal, { sandboxName: uri,
|
||||
@ -6155,11 +6342,13 @@ function AddonInstallWrapper(aInstall) {
|
||||
});
|
||||
#endif
|
||||
|
||||
["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error",
|
||||
["name", "version", "icons", "releaseNotesURI", "file", "state", "error",
|
||||
"progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
|
||||
this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]);
|
||||
}, this);
|
||||
|
||||
this.__defineGetter__("type", () => getExternalType(aInstall.type));
|
||||
|
||||
this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]);
|
||||
|
||||
this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() {
|
||||
@ -6742,7 +6931,7 @@ function AddonWrapper(aAddon) {
|
||||
return [objValue, false];
|
||||
}
|
||||
|
||||
["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
|
||||
["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible",
|
||||
"providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
|
||||
"softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
|
||||
"strictCompatibility", "compatibilityOverrides", "updateURL",
|
||||
@ -6750,6 +6939,8 @@ function AddonWrapper(aAddon) {
|
||||
this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
|
||||
}, this);
|
||||
|
||||
this.__defineGetter__("type", () => getExternalType(aAddon.type));
|
||||
|
||||
["fullDescription", "developerComments", "eula", "supportURL",
|
||||
"contributionURL", "contributionAmount", "averageRating", "reviewCount",
|
||||
"reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
|
||||
|
@ -13,6 +13,7 @@ EXTRA_JS_MODULES.addons += [
|
||||
'GMPProvider.jsm',
|
||||
'LightweightThemeImageOptimizer.jsm',
|
||||
'SpellCheckDictionaryBootstrap.js',
|
||||
'WebExtensionBootstrap.js',
|
||||
]
|
||||
|
||||
# Don't ship unused providers on Android
|
||||
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Web Extension Name",
|
||||
"version": "1.0",
|
||||
"manifest_version": 2,
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "webextension1@tests.mozilla.org"
|
||||
}
|
||||
}
|
||||
}
|
@ -769,6 +769,62 @@ function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
|
||||
return writeInstallRDFToXPI(aData, aDir, aId, aExtraFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a manifest.json manifest into an extension using the properties passed
|
||||
* in a JS object.
|
||||
*
|
||||
* @param aManifest
|
||||
* The data to write
|
||||
* @param aDir
|
||||
* The install directory to add the extension to
|
||||
* @param aId
|
||||
* An optional string to override the default installation aId
|
||||
* @return A file pointing to where the extension was installed
|
||||
*/
|
||||
function writeWebManifestForExtension(aData, aDir, aId = undefined) {
|
||||
if (!aId)
|
||||
aId = aData.applications.gecko.id;
|
||||
|
||||
if (TEST_UNPACKED) {
|
||||
let dir = aDir.clone();
|
||||
dir.append(aId);
|
||||
if (!dir.exists())
|
||||
dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
|
||||
let file = dir.clone();
|
||||
file.append("manifest.json");
|
||||
if (file.exists())
|
||||
file.remove(true);
|
||||
|
||||
let data = JSON.stringify(aData);
|
||||
let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
|
||||
createInstance(AM_Ci.nsIFileOutputStream);
|
||||
fos.init(file,
|
||||
FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
|
||||
FileUtils.PERMS_FILE, 0);
|
||||
fos.write(data, data.length);
|
||||
fos.close();
|
||||
|
||||
return dir;
|
||||
}
|
||||
else {
|
||||
let file = aDir.clone();
|
||||
file.append(aId + ".xpi");
|
||||
|
||||
let stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
|
||||
createInstance(AM_Ci.nsIStringInputStream);
|
||||
stream.setData(JSON.stringify(aData), -1);
|
||||
let zipW = AM_Cc["@mozilla.org/zipwriter;1"].
|
||||
createInstance(AM_Ci.nsIZipWriter);
|
||||
zipW.open(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
|
||||
zipW.addEntryStream("manifest.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
|
||||
stream, false);
|
||||
zipW.close();
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an install.rdf manifest into a packed extension using the properties passed
|
||||
* in a JS object. The objects should contain a property for each property to
|
||||
|
189
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
Normal file
189
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
Normal file
@ -0,0 +1,189 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
const ID = "webextension1@tests.mozilla.org";
|
||||
|
||||
const profileDir = gProfD.clone();
|
||||
profileDir.append("extensions");
|
||||
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
|
||||
startupManager();
|
||||
|
||||
const { GlobalManager, Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
|
||||
|
||||
function promiseAddonStartup() {
|
||||
return new Promise(resolve => {
|
||||
let listener = (extension) => {
|
||||
Management.off("startup", listener);
|
||||
resolve(extension);
|
||||
}
|
||||
|
||||
Management.on("startup", listener);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function*() {
|
||||
do_check_eq(GlobalManager.count, 0);
|
||||
do_check_false(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
yield Promise.all([
|
||||
promiseInstallAllFiles([do_get_addon("webextension_1")], true),
|
||||
promiseAddonStartup()
|
||||
]);
|
||||
|
||||
do_check_eq(GlobalManager.count, 1);
|
||||
do_check_true(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
let addon = yield promiseAddonByID(ID);
|
||||
do_check_neq(addon, null);
|
||||
do_check_eq(addon.version, "1.0");
|
||||
do_check_eq(addon.name, "Web Extension Name");
|
||||
do_check_true(addon.isCompatible);
|
||||
do_check_false(addon.appDisabled);
|
||||
do_check_true(addon.isActive);
|
||||
do_check_eq(addon.type, "extension");
|
||||
do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
|
||||
|
||||
// Should persist through a restart
|
||||
yield promiseShutdownManager();
|
||||
|
||||
do_check_eq(GlobalManager.count, 0);
|
||||
do_check_false(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
startupManager();
|
||||
yield promiseAddonStartup();
|
||||
|
||||
do_check_eq(GlobalManager.count, 1);
|
||||
do_check_true(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
addon = yield promiseAddonByID(ID);
|
||||
do_check_neq(addon, null);
|
||||
do_check_eq(addon.version, "1.0");
|
||||
do_check_eq(addon.name, "Web Extension Name");
|
||||
do_check_true(addon.isCompatible);
|
||||
do_check_false(addon.appDisabled);
|
||||
do_check_true(addon.isActive);
|
||||
do_check_eq(addon.type, "extension");
|
||||
do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
|
||||
|
||||
let file = getFileForAddon(profileDir, ID);
|
||||
do_check_true(file.exists());
|
||||
|
||||
addon.userDisabled = true;
|
||||
|
||||
do_check_eq(GlobalManager.count, 0);
|
||||
do_check_false(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
addon.userDisabled = false;
|
||||
yield promiseAddonStartup();
|
||||
|
||||
do_check_eq(GlobalManager.count, 1);
|
||||
do_check_true(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
addon.uninstall();
|
||||
|
||||
do_check_eq(GlobalManager.count, 0);
|
||||
do_check_false(GlobalManager.extensionMap.has(ID));
|
||||
|
||||
yield promiseShutdownManager();
|
||||
});
|
||||
|
||||
// Writing the manifest direct to the profile should work
|
||||
add_task(function*() {
|
||||
writeWebManifestForExtension({
|
||||
name: "Web Extension Name",
|
||||
version: "1.0",
|
||||
manifest_version: 2,
|
||||
applications: {
|
||||
gecko: {
|
||||
id: ID
|
||||
}
|
||||
}
|
||||
}, profileDir);
|
||||
|
||||
startupManager();
|
||||
|
||||
let addon = yield promiseAddonByID(ID);
|
||||
do_check_neq(addon, null);
|
||||
do_check_eq(addon.version, "1.0");
|
||||
do_check_eq(addon.name, "Web Extension Name");
|
||||
do_check_true(addon.isCompatible);
|
||||
do_check_false(addon.appDisabled);
|
||||
do_check_true(addon.isActive);
|
||||
do_check_eq(addon.type, "extension");
|
||||
do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
|
||||
|
||||
let file = getFileForAddon(profileDir, ID);
|
||||
do_check_true(file.exists());
|
||||
|
||||
addon.uninstall();
|
||||
|
||||
yield promiseRestartManager();
|
||||
});
|
||||
|
||||
// Missing ID should cause a failure
|
||||
add_task(function*() {
|
||||
writeWebManifestForExtension({
|
||||
name: "Web Extension Name",
|
||||
version: "1.0",
|
||||
manifest_version: 2,
|
||||
}, profileDir, ID);
|
||||
|
||||
yield promiseRestartManager();
|
||||
|
||||
let addon = yield promiseAddonByID(ID);
|
||||
do_check_eq(addon, null);
|
||||
|
||||
let file = getFileForAddon(profileDir, ID);
|
||||
do_check_false(file.exists());
|
||||
|
||||
yield promiseRestartManager();
|
||||
});
|
||||
|
||||
// Missing version should cause a failure
|
||||
add_task(function*() {
|
||||
writeWebManifestForExtension({
|
||||
name: "Web Extension Name",
|
||||
manifest_version: 2,
|
||||
applications: {
|
||||
gecko: {
|
||||
id: ID
|
||||
}
|
||||
}
|
||||
}, profileDir);
|
||||
|
||||
yield promiseRestartManager();
|
||||
|
||||
let addon = yield promiseAddonByID(ID);
|
||||
do_check_eq(addon, null);
|
||||
|
||||
let file = getFileForAddon(profileDir, ID);
|
||||
do_check_false(file.exists());
|
||||
|
||||
yield promiseRestartManager();
|
||||
});
|
||||
|
||||
// Incorrect manifest version should cause a failure
|
||||
add_task(function*() {
|
||||
writeWebManifestForExtension({
|
||||
name: "Web Extension Name",
|
||||
version: "1.0",
|
||||
manifest_version: 1,
|
||||
applications: {
|
||||
gecko: {
|
||||
id: ID
|
||||
}
|
||||
}
|
||||
}, profileDir);
|
||||
|
||||
yield promiseRestartManager();
|
||||
|
||||
let addon = yield promiseAddonByID(ID);
|
||||
do_check_eq(addon, null);
|
||||
|
||||
let file = getFileForAddon(profileDir, ID);
|
||||
do_check_false(file.exists());
|
||||
|
||||
yield promiseRestartManager();
|
||||
});
|
@ -285,4 +285,5 @@ run-sequentially = Uses global XCurProcD dir.
|
||||
[test_overrideblocklist.js]
|
||||
run-sequentially = Uses global XCurProcD dir.
|
||||
[test_sourceURI.js]
|
||||
[test_webextension.js]
|
||||
[test_bootstrap_globals.js]
|
||||
|
@ -26,4 +26,5 @@ skip-if = appname != "firefox"
|
||||
[test_XPIcancel.js]
|
||||
[test_XPIStates.js]
|
||||
|
||||
|
||||
[include:xpcshell-shared.ini]
|
||||
|
Loading…
x
Reference in New Issue
Block a user