зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1645054 - Disable/purge service workers when dom.serviceWorkers.enabled is false r=dom-worker-reviewers,necko-reviewers,asuth,webdriver-reviewers,whimboo
Differential Revision: https://phabricator.services.mozilla.com/D167550
This commit is contained in:
Родитель
3575a37276
Коммит
b0c912c415
|
@ -468,10 +468,6 @@ void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
|
|||
|
||||
MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
|
||||
|
||||
nsTArray<ServiceWorkerRegistrationData> data;
|
||||
aRegistrar->GetRegistrations(data);
|
||||
LoadRegistrations(data);
|
||||
|
||||
PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
|
||||
if (NS_WARN_IF(!actorChild)) {
|
||||
MaybeStartShutdown();
|
||||
|
@ -487,6 +483,12 @@ void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
|
|||
|
||||
mActor = static_cast<ServiceWorkerManagerChild*>(actor);
|
||||
|
||||
// mActor must be set before LoadRegistrations is called because it can purge
|
||||
// service workers if preferences are disabled.
|
||||
nsTArray<ServiceWorkerRegistrationData> data;
|
||||
aRegistrar->GetRegistrations(data);
|
||||
LoadRegistrations(data);
|
||||
|
||||
mTelemetryLastChange = TimeStamp::Now();
|
||||
}
|
||||
|
||||
|
@ -1365,8 +1367,8 @@ ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal,
|
|||
NS_ConvertUTF16toUTF8 scope(aScope);
|
||||
RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope);
|
||||
|
||||
RefPtr<ServiceWorkerUnregisterJob> job = new ServiceWorkerUnregisterJob(
|
||||
aPrincipal, scope, true /* send to parent */);
|
||||
RefPtr<ServiceWorkerUnregisterJob> job =
|
||||
new ServiceWorkerUnregisterJob(aPrincipal, scope);
|
||||
|
||||
if (aCallback) {
|
||||
RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback);
|
||||
|
@ -1498,6 +1500,14 @@ void ServiceWorkerManager::HandleError(
|
|||
aColumnNumber, aFlags);
|
||||
}
|
||||
|
||||
void ServiceWorkerManager::PurgeServiceWorker(
|
||||
const ServiceWorkerRegistrationData& aRegistration,
|
||||
nsIPrincipal* aPrincipal) {
|
||||
MOZ_ASSERT(mActor);
|
||||
serviceWorkerScriptCache::PurgeCache(aPrincipal, aRegistration.cacheName());
|
||||
MaybeSendUnregister(aPrincipal, aRegistration.scope());
|
||||
}
|
||||
|
||||
void ServiceWorkerManager::LoadRegistration(
|
||||
const ServiceWorkerRegistrationData& aRegistration) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
@ -1508,6 +1518,13 @@ void ServiceWorkerManager::LoadRegistration(
|
|||
}
|
||||
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
|
||||
|
||||
if (!StaticPrefs::dom_serviceWorkers_enabled()) {
|
||||
// If service workers are disabled, remove the registration from disk
|
||||
// instead of loading.
|
||||
PurgeServiceWorker(aRegistration, principal);
|
||||
return;
|
||||
}
|
||||
|
||||
// Purge extensions registrations if they are disabled by prefs.
|
||||
if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
|
||||
nsCOMPtr<nsIURI> uri = principal->GetURI();
|
||||
|
@ -1516,8 +1533,7 @@ void ServiceWorkerManager::LoadRegistration(
|
|||
// the extension may not have been loaded yet and the WebExtensionPolicy
|
||||
// may not exist yet.
|
||||
if (uri->SchemeIs("moz-extension")) {
|
||||
const auto& cacheName = aRegistration.cacheName();
|
||||
serviceWorkerScriptCache::PurgeCache(principal, cacheName);
|
||||
PurgeServiceWorker(aRegistration, principal);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -355,6 +355,9 @@ class ServiceWorkerManager final : public nsIServiceWorkerManager,
|
|||
|
||||
void MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);
|
||||
|
||||
void PurgeServiceWorker(const ServiceWorkerRegistrationData& aRegistration,
|
||||
nsIPrincipal* aPrincipal);
|
||||
|
||||
RefPtr<ServiceWorkerManagerChild> mActor;
|
||||
|
||||
bool mShuttingDown;
|
||||
|
|
|
@ -43,11 +43,9 @@ NS_IMPL_ISUPPORTS(ServiceWorkerUnregisterJob::PushUnsubscribeCallback,
|
|||
nsIUnsubscribeResultCallback)
|
||||
|
||||
ServiceWorkerUnregisterJob::ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal,
|
||||
const nsACString& aScope,
|
||||
bool aSendToParent)
|
||||
const nsACString& aScope)
|
||||
: ServiceWorkerJob(Type::Unregister, aPrincipal, aScope, ""_ns),
|
||||
mResult(false),
|
||||
mSendToParent(aSendToParent) {}
|
||||
mResult(false) {}
|
||||
|
||||
bool ServiceWorkerUnregisterJob::GetResult() const {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
@ -110,9 +108,7 @@ void ServiceWorkerUnregisterJob::Unregister() {
|
|||
// Note, we send the message to remove the registration from disk now. This is
|
||||
// necessary to ensure the registration is removed if the controlled
|
||||
// clients are closed by shutting down the browser.
|
||||
if (mSendToParent) {
|
||||
swm->MaybeSendUnregister(mPrincipal, mScope);
|
||||
}
|
||||
swm->MaybeSendUnregister(mPrincipal, mScope);
|
||||
|
||||
swm->EvictFromBFCache(registration);
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ namespace mozilla::dom {
|
|||
|
||||
class ServiceWorkerUnregisterJob final : public ServiceWorkerJob {
|
||||
public:
|
||||
ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, const nsACString& aScope,
|
||||
bool aSendToParent);
|
||||
ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal,
|
||||
const nsACString& aScope);
|
||||
|
||||
bool GetResult() const;
|
||||
|
||||
|
@ -28,7 +28,6 @@ class ServiceWorkerUnregisterJob final : public ServiceWorkerJob {
|
|||
void Unregister();
|
||||
|
||||
bool mResult;
|
||||
bool mSendToParent;
|
||||
};
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -218,11 +218,15 @@ support-files =
|
|||
self_update_worker.sjs
|
||||
!/dom/events/test/event_leak_utils.js
|
||||
onmessageerror_worker.js
|
||||
pref/fetch_nonexistent_file.html
|
||||
pref/intercept_nonexistent_file_sw.js
|
||||
|
||||
[test_abrupt_completion.html]
|
||||
skip-if =
|
||||
os == 'linux' #Bug 1615164
|
||||
win10_2004 # Bug 1615164
|
||||
[test_async_waituntil.html]
|
||||
[test_bad_script_cache.html]
|
||||
[test_bug1151916.html]
|
||||
[test_bug1240436.html]
|
||||
[test_bug1408734.html]
|
||||
|
@ -232,6 +236,7 @@ skip-if =
|
|||
[test_cross_origin_url_after_redirect.html]
|
||||
[test_devtools_bypass_serviceworker.html]
|
||||
[test_empty_serviceworker.html]
|
||||
[test_enabled_pref.html]
|
||||
[test_error_reporting.html]
|
||||
skip-if = serviceworker_e10s
|
||||
[test_escapedSlashes.html]
|
||||
|
@ -251,6 +256,9 @@ skip-if = serviceworker_e10s
|
|||
support-files = console_monitor.js
|
||||
[test_file_blob_response.html]
|
||||
[test_file_blob_upload.html]
|
||||
[test_file_upload.html]
|
||||
skip-if = toolkit == 'android' #Bug 1430182
|
||||
support-files = script_file_upload.js sw_file_upload.js server_file_upload.sjs
|
||||
[test_force_refresh.html]
|
||||
[test_gzip_redirect.html]
|
||||
[test_hsts_upgrade_intercept.html]
|
||||
|
@ -280,6 +288,7 @@ scheme = https
|
|||
skip-if =
|
||||
os == "linux" && bits == 64 && debug # Bug 1749068
|
||||
[test_navigator.html]
|
||||
[test_nofetch_handler.html]
|
||||
[test_not_intercept_plugin.html]
|
||||
skip-if = serviceworker_e10s # leaks InterceptedHttpChannel and others things
|
||||
[test_notification_constructor_error.html]
|
||||
|
@ -291,12 +300,12 @@ skip-if =
|
|||
xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object
|
||||
support-files = notification_openWindow_worker.js file_notification_openWindow.html
|
||||
tags = openwindow
|
||||
[test_notificationclick-otherwindow.html]
|
||||
skip-if = xorigin # Bug 1792790
|
||||
[test_notificationclick.html]
|
||||
skip-if = xorigin # Bug 1792790
|
||||
[test_notificationclick_focus.html]
|
||||
skip-if = xorigin # Bug 1792790
|
||||
[test_notificationclick-otherwindow.html]
|
||||
skip-if = xorigin # Bug 1792790
|
||||
[test_notificationclose.html]
|
||||
skip-if = xorigin # Bug 1792790
|
||||
[test_onmessageerror.html]
|
||||
|
@ -312,12 +321,15 @@ skip-if = xorigin # Hangs with no error log
|
|||
[test_register_base.html]
|
||||
[test_register_https_in_http.html]
|
||||
[test_sandbox_intercept.html]
|
||||
[test_sanitize.html]
|
||||
[test_scopes.html]
|
||||
[test_script_loader_intercepted_js_cache.html]
|
||||
skip-if = serviceworker_e10s
|
||||
[test_sanitize.html]
|
||||
[test_serviceworker.html]
|
||||
[test_self_update_worker.html]
|
||||
skip-if = serviceworker_e10s
|
||||
toolkit == 'android'
|
||||
[test_service_worker_allowed.html]
|
||||
[test_serviceworker.html]
|
||||
[test_serviceworker_header.html]
|
||||
[test_serviceworker_interfaces.html]
|
||||
[test_serviceworker_not_sharedworker.html]
|
||||
|
@ -333,15 +345,6 @@ skip-if = verify
|
|||
serviceworker_e10s
|
||||
[test_workerUnregister.html]
|
||||
[test_workerUpdate.html]
|
||||
[test_worker_reference_gc_timeout.html]
|
||||
[test_workerupdatefoundevent.html]
|
||||
[test_xslt.html]
|
||||
[test_async_waituntil.html]
|
||||
[test_worker_reference_gc_timeout.html]
|
||||
[test_nofetch_handler.html]
|
||||
[test_bad_script_cache.html]
|
||||
[test_file_upload.html]
|
||||
skip-if = toolkit == 'android' #Bug 1430182
|
||||
support-files = script_file_upload.js sw_file_upload.js server_file_upload.sjs
|
||||
[test_self_update_worker.html]
|
||||
skip-if = serviceworker_e10s
|
||||
toolkit == 'android'
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
|
||||
async function fetch_status() {
|
||||
let response = await fetch('this_file_does_not_exist.txt');
|
||||
return response.status;
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
onfetch = function(e) {
|
||||
if (e.request.url.match(/this_file_does_not_exist.txt$/)) {
|
||||
e.respondWith(new Response("intercepted"));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bug 1645054 - test dom.serviceWorkers.enabled preference</title>
|
||||
</head>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script>
|
||||
|
||||
function create_iframe(url) {
|
||||
return new Promise(function(res) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.src = url;
|
||||
iframe.onload = function() { res(iframe) }
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
async function do_fetch(pref) {
|
||||
await SpecialPowers.pushPrefEnv({ set: [pref] });
|
||||
|
||||
let iframe = await create_iframe("./pref/fetch_nonexistent_file.html");
|
||||
let status = await iframe.contentWindow.fetch_status();
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
return status;
|
||||
}
|
||||
|
||||
add_task(async () => {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [['dom.serviceWorkers.testing.enabled', true]]
|
||||
});
|
||||
|
||||
let reg = await navigator.serviceWorker.register(
|
||||
'pref/intercept_nonexistent_file_sw.js');
|
||||
await waitForState(reg.installing, 'activated');
|
||||
|
||||
let status;
|
||||
|
||||
status = await do_fetch(['dom.serviceWorkers.enabled', true]);
|
||||
is(status, 200, 'SW enabled');
|
||||
|
||||
status = await do_fetch(['dom.serviceWorkers.enabled', false]);
|
||||
is(status, 404, 'SW disabled');
|
||||
|
||||
status = await do_fetch(['dom.serviceWorkers.enabled', true]);
|
||||
is(status, 200, 'SW enabled again');
|
||||
|
||||
await reg.unregister();
|
||||
});
|
||||
|
||||
</script>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -1 +1,2 @@
|
|||
[test_service_workers_at_startup.py]
|
||||
[test_service_workers_disabled.py]
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# 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 os
|
||||
|
||||
from marionette_driver import Wait
|
||||
from marionette_harness import MarionetteTestCase
|
||||
|
||||
|
||||
class MarionetteServiceWorkerTestCase(MarionetteTestCase):
|
||||
def install_service_worker(self, path):
|
||||
install_url = self.marionette.absolute_url(path)
|
||||
self.marionette.navigate(install_url)
|
||||
Wait(self.marionette).until(
|
||||
lambda _: self.is_service_worker_registered,
|
||||
message="Service worker not successfully installed",
|
||||
)
|
||||
|
||||
# Wait for the registered service worker to be stored in the Firefox
|
||||
# profile before restarting the instance to prevent intermittent
|
||||
# failures (Bug 1665184).
|
||||
Wait(self.marionette, timeout=10).until(
|
||||
lambda _: self.profile_serviceworker_txt_exists,
|
||||
message="Service worker not stored in profile",
|
||||
)
|
||||
|
||||
# self.marionette.restart(in_app=True) will restore service workers if
|
||||
# we don't navigate away before restarting.
|
||||
self.marionette.navigate("about:blank")
|
||||
|
||||
# Using @property helps avoid the case where missing parens at the call site
|
||||
# yields an unvarying 'true' value.
|
||||
@property
|
||||
def profile_serviceworker_txt_exists(self):
|
||||
return "serviceworker.txt" in os.listdir(self.marionette.profile_path)
|
||||
|
||||
@property
|
||||
def is_service_worker_registered(self):
|
||||
with self.marionette.using_context("chrome"):
|
||||
return self.marionette.execute_script(
|
||||
"""
|
||||
let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
|
||||
Ci.nsIServiceWorkerManager
|
||||
);
|
||||
let ssm = Services.scriptSecurityManager;
|
||||
|
||||
let principal = ssm.createContentPrincipalFromOrigin(arguments[0]);
|
||||
|
||||
let serviceWorkers = swm.getAllRegistrations();
|
||||
for (let i = 0; i < serviceWorkers.length; i++) {
|
||||
let sw = serviceWorkers.queryElementAt(
|
||||
i,
|
||||
Ci.nsIServiceWorkerRegistrationInfo
|
||||
);
|
||||
if (sw.principal.origin == principal.origin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
""",
|
||||
script_args=(self.marionette.absolute_url(""),),
|
||||
)
|
|
@ -3,80 +3,29 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add this directory to the import path.
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from marionette_driver import Wait
|
||||
from marionette_harness import MarionetteTestCase
|
||||
from service_worker_utils import MarionetteServiceWorkerTestCase
|
||||
|
||||
|
||||
class ServiceWorkerAtStartupTestCase(MarionetteTestCase):
|
||||
class ServiceWorkerAtStartupTestCase(MarionetteServiceWorkerTestCase):
|
||||
def setUp(self):
|
||||
super(ServiceWorkerAtStartupTestCase, self).setUp()
|
||||
self.install_service_worker()
|
||||
self.install_service_worker("serviceworker/install_serviceworker.html")
|
||||
|
||||
def tearDown(self):
|
||||
self.marionette.restart(in_app=False, clean=True)
|
||||
super(ServiceWorkerAtStartupTestCase, self).tearDown()
|
||||
|
||||
def install_service_worker(self):
|
||||
install_url = self.marionette.absolute_url(
|
||||
"serviceworker/install_serviceworker.html"
|
||||
)
|
||||
self.marionette.navigate(install_url)
|
||||
Wait(self.marionette).until(
|
||||
lambda _: self.is_service_worker_registered,
|
||||
message="Wait the service worker to be installed",
|
||||
)
|
||||
|
||||
def test_registered_service_worker_after_restart(self):
|
||||
# Wait the registered service worker to be stored in the Firefox profile
|
||||
# before restarting the instance to prevent intermittent failures
|
||||
# (Bug 1665184).
|
||||
Wait(self.marionette, timeout=10).until(
|
||||
lambda _: self.profile_serviceworker_txt_exists,
|
||||
message="Wait service workers to be stored in the profile",
|
||||
)
|
||||
|
||||
# Quit and start a new session to simulate a full browser restart
|
||||
# (`self.marionette.restart()` seems to not
|
||||
# be enough to simulate this scenario, because the service workers
|
||||
# are staying registered and they are not actually re-registered
|
||||
# from the list stored in the profile as this test needs).
|
||||
self.marionette.quit()
|
||||
self.marionette.start_session()
|
||||
self.marionette.restart()
|
||||
|
||||
Wait(self.marionette).until(
|
||||
lambda _: self.is_service_worker_registered,
|
||||
message="Wait the service worker to be registered after restart",
|
||||
message="Service worker not registered after restart",
|
||||
)
|
||||
self.assertTrue(self.is_service_worker_registered)
|
||||
|
||||
@property
|
||||
def profile_serviceworker_txt_exists(self):
|
||||
return "serviceworker.txt" in os.listdir(self.marionette.profile_path)
|
||||
|
||||
@property
|
||||
def is_service_worker_registered(self):
|
||||
with self.marionette.using_context("chrome"):
|
||||
return self.marionette.execute_script(
|
||||
"""
|
||||
let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
|
||||
Ci.nsIServiceWorkerManager
|
||||
);
|
||||
let ssm = Services.scriptSecurityManager;
|
||||
|
||||
let principal = ssm.createContentPrincipalFromOrigin(arguments[0]);
|
||||
|
||||
let serviceWorkers = swm.getAllRegistrations();
|
||||
for (let i = 0; i < serviceWorkers.length; i++) {
|
||||
let sw = serviceWorkers.queryElementAt(
|
||||
i,
|
||||
Ci.nsIServiceWorkerRegistrationInfo
|
||||
);
|
||||
if (sw.principal.origin == principal.origin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
""",
|
||||
script_args=(self.marionette.absolute_url(""),),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# 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 os
|
||||
import sys
|
||||
|
||||
# Add this directory to the import path.
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from service_worker_utils import MarionetteServiceWorkerTestCase
|
||||
|
||||
|
||||
class ServiceWorkersDisabledTestCase(MarionetteServiceWorkerTestCase):
|
||||
def setUp(self):
|
||||
super(ServiceWorkersDisabledTestCase, self).setUp()
|
||||
self.install_service_worker("serviceworker/install_serviceworker.html")
|
||||
|
||||
def tearDown(self):
|
||||
self.marionette.restart(in_app=False, clean=True)
|
||||
super(ServiceWorkersDisabledTestCase, self).tearDown()
|
||||
|
||||
def test_service_workers_disabled_at_startup(self):
|
||||
# self.marionette.set_pref sets preferences after startup. Using it
|
||||
# here causes intermittent failures.
|
||||
self.marionette.instance.profile.set_preferences(
|
||||
{
|
||||
"dom.serviceWorkers.enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.marionette.restart()
|
||||
|
||||
self.assertFalse(
|
||||
self.is_service_worker_registered,
|
||||
"Service worker registration should have been purged",
|
||||
)
|
|
@ -4012,6 +4012,10 @@ bool HttpBaseChannel::ShouldIntercept(nsIURI* aURI) {
|
|||
GetCallback(controller);
|
||||
bool shouldIntercept = false;
|
||||
|
||||
if (!StaticPrefs::dom_serviceWorkers_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should never intercept internal redirects. The ServiceWorker code
|
||||
// can trigger interntal redirects as the result of a FetchEvent. If
|
||||
// we re-intercept then an infinite loop can occur.
|
||||
|
|
|
@ -28,6 +28,14 @@ support-files =
|
|||
test_http3_prio_helpers.js
|
||||
http2_test_common.js
|
||||
|
||||
# dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml
|
||||
# and enabled individually by app prefs, so for the xpcshell tests that involve
|
||||
# interception, we need to explicitly enable the pref.
|
||||
# Consider enabling it in StaticPrefList.yaml
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=1816325
|
||||
prefs =
|
||||
dom.serviceWorkers.enabled=true
|
||||
|
||||
[test_trr_nat64.js]
|
||||
run-sequentially = node server exceptions dont replay well
|
||||
[test_nsIBufferedOutputStream_writeFrom_block.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче