зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changeset 326615f76fd9 (bug 1333981) for asserting in test_storageNotifications.html. r=backout
This commit is contained in:
Родитель
f85d81d30e
Коммит
f48d2ca17a
|
@ -23,7 +23,6 @@
|
|||
#include "mozilla/dom/Performance.h"
|
||||
#include "mozilla/dom/StorageEvent.h"
|
||||
#include "mozilla/dom/StorageEventBinding.h"
|
||||
#include "mozilla/dom/StorageNotifierService.h"
|
||||
#include "mozilla/dom/Timeout.h"
|
||||
#include "mozilla/dom/TimeoutHandler.h"
|
||||
#include "mozilla/dom/TimeoutManager.h"
|
||||
|
@ -465,9 +464,8 @@ static NS_DEFINE_CID(kXULControllersCID, NS_XULCONTROLLERS_CID);
|
|||
* An indirect observer object that means we don't have to implement nsIObserver
|
||||
* on nsGlobalWindow, where any script could see it.
|
||||
*/
|
||||
class nsGlobalWindowObserver final : public nsIObserver
|
||||
, public nsIInterfaceRequestor
|
||||
, public StorageNotificationObserver
|
||||
class nsGlobalWindowObserver final : public nsIObserver,
|
||||
public nsIInterfaceRequestor
|
||||
{
|
||||
public:
|
||||
explicit nsGlobalWindowObserver(nsGlobalWindow* aWindow) : mWindow(aWindow) {}
|
||||
|
@ -487,23 +485,6 @@ public:
|
|||
return NS_NOINTERFACE;
|
||||
}
|
||||
|
||||
void
|
||||
ObserveStorageNotification(StorageEvent* aEvent,
|
||||
const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing) override
|
||||
{
|
||||
if (mWindow) {
|
||||
mWindow->ObserveStorageNotification(aEvent, aStorageType,
|
||||
aPrivateBrowsing);
|
||||
}
|
||||
}
|
||||
|
||||
virtual nsIEventTarget*
|
||||
GetEventTarget() const override
|
||||
{
|
||||
return mWindow ? mWindow->EventTargetFor(TaskCategory::Other) : nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
~nsGlobalWindowObserver() = default;
|
||||
|
||||
|
@ -1650,16 +1631,14 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalWindow *aOuterWindow)
|
|||
// a strong reference.
|
||||
os->AddObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC,
|
||||
false);
|
||||
|
||||
// Watch for dom-storage2-changed and dom-private-storage2-changed so we
|
||||
// can fire storage events. Use a strong reference.
|
||||
os->AddObserver(mObserver, "dom-storage2-changed", false);
|
||||
os->AddObserver(mObserver, "dom-private-storage2-changed", false);
|
||||
}
|
||||
|
||||
Preferences::AddStrongObserver(mObserver, "intl.accept_languages");
|
||||
|
||||
// Watch for storage notifications so we can fire storage events.
|
||||
RefPtr<StorageNotifierService> sns =
|
||||
StorageNotifierService::GetOrCreate();
|
||||
if (sns) {
|
||||
sns->Register(mObserver);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// |this| is an outer window. Outer windows start out frozen and
|
||||
|
@ -1955,11 +1934,8 @@ nsGlobalWindow::CleanUp()
|
|||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||||
if (os) {
|
||||
os->RemoveObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
|
||||
}
|
||||
|
||||
RefPtr<StorageNotifierService> sns = StorageNotifierService::GetOrCreate();
|
||||
if (sns) {
|
||||
sns->Unregister(mObserver);
|
||||
os->RemoveObserver(mObserver, "dom-storage2-changed");
|
||||
os->RemoveObserver(mObserver, "dom-private-storage2-changed");
|
||||
}
|
||||
|
||||
#ifdef MOZ_B2G
|
||||
|
@ -12293,6 +12269,131 @@ nsGlobalWindow::Observe(nsISupports* aSubject, const char* aTopic,
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
// We need these for our private-browsing check below; so save them off.
|
||||
// (And we can't do the private-browsing enforcement check as part of our
|
||||
// outer conditional because otherwise we'll trigger an NS_WARNING if control
|
||||
// flow reaches the bottom of this method.)
|
||||
bool isNonPrivateLocalStorageChange =
|
||||
!nsCRT::strcmp(aTopic, "dom-storage2-changed");
|
||||
bool isPrivateLocalStorageChange =
|
||||
!nsCRT::strcmp(aTopic, "dom-private-storage2-changed");
|
||||
if (isNonPrivateLocalStorageChange || isPrivateLocalStorageChange) {
|
||||
// Enforce that the source storage area's private browsing state matches
|
||||
// this window's state. These flag checks and their maintenance independent
|
||||
// from the principal's OriginAttributes matter because chrome docshells
|
||||
// that are part of private browsing windows can be private browsing without
|
||||
// having their OriginAttributes set (because they have the system
|
||||
// principal).
|
||||
bool isPrivateBrowsing = IsPrivateBrowsing();
|
||||
if ((isNonPrivateLocalStorageChange && isPrivateBrowsing) ||
|
||||
(isPrivateLocalStorageChange && !isPrivateBrowsing)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// We require that aData be either u"SessionStorage" or u"localStorage".
|
||||
// Assert under debug, but ignore the bogus event under non-debug.
|
||||
MOZ_ASSERT(aData);
|
||||
if (!aData) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// LocalStorage can only exist on an inner window, and we don't want to
|
||||
// generate events on frozen or otherwise-navigated-away from windows.
|
||||
// (Actually, this code used to try and buffer events for frozen windows,
|
||||
// but it never worked, so we've removed it. See bug 1285898.)
|
||||
if (!IsInnerWindow() || !AsInner()->IsCurrentInnerWindow() || IsFrozen()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsIPrincipal *principal = GetPrincipal();
|
||||
if (!principal) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
RefPtr<StorageEvent> event = static_cast<StorageEvent*>(aSubject);
|
||||
if (!event) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
bool fireMozStorageChanged = false;
|
||||
nsAutoString eventType;
|
||||
eventType.AssignLiteral("storage");
|
||||
|
||||
if (!NS_strcmp(aData, u"sessionStorage")) {
|
||||
nsCOMPtr<nsIDOMStorage> changingStorage = event->GetStorageArea();
|
||||
MOZ_ASSERT(changingStorage);
|
||||
|
||||
bool check = false;
|
||||
|
||||
nsCOMPtr<nsIDOMStorageManager> storageManager = do_QueryInterface(GetDocShell());
|
||||
if (storageManager) {
|
||||
nsresult rv = storageManager->CheckStorage(principal, changingStorage,
|
||||
&check);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
if (!check) {
|
||||
// This storage event is not coming from our storage or is coming
|
||||
// from a different docshell, i.e. it is a clone, ignore this event.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
MOZ_LOG(gDOMLeakPRLog, LogLevel::Debug,
|
||||
("nsGlobalWindow %p with sessionStorage %p passing event from %p",
|
||||
this, mSessionStorage.get(), changingStorage.get()));
|
||||
|
||||
fireMozStorageChanged = mSessionStorage == changingStorage;
|
||||
if (fireMozStorageChanged) {
|
||||
eventType.AssignLiteral("MozSessionStorageChanged");
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
MOZ_ASSERT(!NS_strcmp(aData, u"localStorage"));
|
||||
nsIPrincipal* storagePrincipal = event->GetPrincipal();
|
||||
if (!storagePrincipal) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool equals = false;
|
||||
nsresult rv = storagePrincipal->Equals(principal, &equals);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (!equals) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
fireMozStorageChanged = mLocalStorage == event->GetStorageArea();
|
||||
|
||||
if (fireMozStorageChanged) {
|
||||
eventType.AssignLiteral("MozLocalStorageChanged");
|
||||
}
|
||||
}
|
||||
|
||||
// Clone the storage event included in the observer notification. We want
|
||||
// to dispatch clones rather than the original event.
|
||||
ErrorResult error;
|
||||
RefPtr<StorageEvent> clonedEvent =
|
||||
CloneStorageEvent(eventType, event, error);
|
||||
if (error.Failed()) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
clonedEvent->SetTrusted(true);
|
||||
|
||||
if (fireMozStorageChanged) {
|
||||
WidgetEvent* internalEvent = clonedEvent->WidgetEventPtr();
|
||||
internalEvent->mFlags.mOnlyChromeDispatch = true;
|
||||
}
|
||||
|
||||
bool defaultActionEnabled;
|
||||
DispatchEvent(clonedEvent, &defaultActionEnabled);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (!nsCRT::strcmp(aTopic, "offline-cache-update-added")) {
|
||||
if (mApplicationCache)
|
||||
return NS_OK;
|
||||
|
@ -12362,114 +12463,6 @@ nsGlobalWindow::Observe(nsISupports* aSubject, const char* aTopic,
|
|||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
void
|
||||
nsGlobalWindow::ObserveStorageNotification(StorageEvent* aEvent,
|
||||
const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing)
|
||||
{
|
||||
MOZ_ASSERT(aEvent);
|
||||
|
||||
// Enforce that the source storage area's private browsing state matches
|
||||
// this window's state. These flag checks and their maintenance independent
|
||||
// from the principal's OriginAttributes matter because chrome docshells
|
||||
// that are part of private browsing windows can be private browsing without
|
||||
// having their OriginAttributes set (because they have the system
|
||||
// principal).
|
||||
bool isPrivateBrowsing = IsPrivateBrowsing();
|
||||
if (isPrivateBrowsing != aPrivateBrowsing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// LocalStorage can only exist on an inner window, and we don't want to
|
||||
// generate events on frozen or otherwise-navigated-away from windows.
|
||||
// (Actually, this code used to try and buffer events for frozen windows,
|
||||
// but it never worked, so we've removed it. See bug 1285898.)
|
||||
if (!IsInnerWindow() || !AsInner()->IsCurrentInnerWindow() || IsFrozen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
nsIPrincipal *principal = GetPrincipal();
|
||||
if (!principal) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool fireMozStorageChanged = false;
|
||||
nsAutoString eventType;
|
||||
eventType.AssignLiteral("storage");
|
||||
|
||||
if (!NS_strcmp(aStorageType, u"sessionStorage")) {
|
||||
nsCOMPtr<nsIDOMStorage> changingStorage = aEvent->GetStorageArea();
|
||||
MOZ_ASSERT(changingStorage);
|
||||
|
||||
bool check = false;
|
||||
|
||||
nsCOMPtr<nsIDOMStorageManager> storageManager = do_QueryInterface(GetDocShell());
|
||||
if (storageManager) {
|
||||
nsresult rv = storageManager->CheckStorage(principal, changingStorage,
|
||||
&check);
|
||||
if (NS_FAILED(rv)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!check) {
|
||||
// This storage event is not coming from our storage or is coming
|
||||
// from a different docshell, i.e. it is a clone, ignore this event.
|
||||
return;
|
||||
}
|
||||
|
||||
MOZ_LOG(gDOMLeakPRLog, LogLevel::Debug,
|
||||
("nsGlobalWindow %p with sessionStorage %p passing event from %p",
|
||||
this, mSessionStorage.get(), changingStorage.get()));
|
||||
|
||||
fireMozStorageChanged = mSessionStorage == changingStorage;
|
||||
if (fireMozStorageChanged) {
|
||||
eventType.AssignLiteral("MozSessionStorageChanged");
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
MOZ_ASSERT(!NS_strcmp(aStorageType, u"localStorage"));
|
||||
nsIPrincipal* storagePrincipal = aEvent->GetPrincipal();
|
||||
if (!storagePrincipal) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool equals = false;
|
||||
nsresult rv = storagePrincipal->Equals(principal, &equals);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
|
||||
if (!equals) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireMozStorageChanged = mLocalStorage == aEvent->GetStorageArea();
|
||||
|
||||
if (fireMozStorageChanged) {
|
||||
eventType.AssignLiteral("MozLocalStorageChanged");
|
||||
}
|
||||
}
|
||||
|
||||
// Clone the storage event included in the observer notification. We want
|
||||
// to dispatch clones rather than the original event.
|
||||
IgnoredErrorResult error;
|
||||
RefPtr<StorageEvent> clonedEvent =
|
||||
CloneStorageEvent(eventType, aEvent, error);
|
||||
if (error.Failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clonedEvent->SetTrusted(true);
|
||||
|
||||
if (fireMozStorageChanged) {
|
||||
WidgetEvent* internalEvent = clonedEvent->WidgetEventPtr();
|
||||
internalEvent->mFlags.mOnlyChromeDispatch = true;
|
||||
}
|
||||
|
||||
bool defaultActionEnabled;
|
||||
DispatchEvent(clonedEvent, &defaultActionEnabled);
|
||||
}
|
||||
|
||||
already_AddRefed<StorageEvent>
|
||||
nsGlobalWindow::CloneStorageEvent(const nsAString& aType,
|
||||
const RefPtr<StorageEvent>& aEvent,
|
||||
|
|
|
@ -611,10 +611,6 @@ public:
|
|||
nsresult Observe(nsISupports* aSubject, const char* aTopic,
|
||||
const char16_t* aData);
|
||||
|
||||
void ObserveStorageNotification(mozilla::dom::StorageEvent* aEvent,
|
||||
const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing);
|
||||
|
||||
// Outer windows only.
|
||||
void UnblockScriptedClosing();
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "Storage.h"
|
||||
#include "StorageNotifierService.h"
|
||||
|
||||
#include "mozilla/dom/StorageBinding.h"
|
||||
#include "nsIPrincipal.h"
|
||||
|
@ -122,21 +121,13 @@ Storage::NotifyChange(Storage* aStorage, nsIPrincipal* aPrincipal,
|
|||
|
||||
event->SetPrincipal(aPrincipal);
|
||||
|
||||
// This will send the event to any registered window.
|
||||
StorageNotifierService::Broadcast(event, aStorageType, aIsPrivate,
|
||||
aImmediateDispatch);
|
||||
|
||||
// This runnable is mainly used by devtools. Windows receive notification by
|
||||
// StorageNotifierService.
|
||||
|
||||
RefPtr<StorageNotifierRunnable> r =
|
||||
new StorageNotifierRunnable(event, aStorageType, aIsPrivate);
|
||||
|
||||
if (aImmediateDispatch) {
|
||||
Unused << r->Run();
|
||||
} else {
|
||||
SystemGroup::Dispatch("Storage::NotifyChange", TaskCategory::Other,
|
||||
r.forget());
|
||||
NS_DispatchToMainThread(r, NS_DISPATCH_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "StorageNotifierService.h"
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/StaticPtr.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
namespace {
|
||||
|
||||
// This boolean is used to avoid the creation of the service after been
|
||||
// distroyed on shutdown.
|
||||
bool gStorageShuttingDown = false;
|
||||
|
||||
StaticRefPtr<StorageNotifierService> gStorageNotifierService;
|
||||
|
||||
} // anonymous
|
||||
|
||||
/* static */ StorageNotifierService*
|
||||
StorageNotifierService::GetOrCreate()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (!gStorageNotifierService && !gStorageShuttingDown) {
|
||||
gStorageNotifierService = new StorageNotifierService();
|
||||
ClearOnShutdown(&gStorageNotifierService);
|
||||
}
|
||||
|
||||
return gStorageNotifierService;
|
||||
}
|
||||
|
||||
StorageNotifierService::StorageNotifierService()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!gStorageNotifierService);
|
||||
}
|
||||
|
||||
StorageNotifierService::~StorageNotifierService()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!gStorageNotifierService);
|
||||
gStorageShuttingDown = true;
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
StorageNotifierService::Broadcast(StorageEvent* aEvent,
|
||||
const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing,
|
||||
bool aImmediateDispatch)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
RefPtr<StorageNotifierService> service = gStorageNotifierService;
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
|
||||
RefPtr<StorageEvent> event = aEvent;
|
||||
|
||||
nsTObserverArray<RefPtr<StorageNotificationObserver>>::ForwardIterator
|
||||
iter(service->mObservers);
|
||||
|
||||
while (iter.HasMore()) {
|
||||
RefPtr<StorageNotificationObserver> observer = iter.GetNext();
|
||||
|
||||
RefPtr<Runnable> r = NS_NewRunnableFunction(
|
||||
"StorageNotifierService::Broadcast",
|
||||
[observer, event, aStorageType, aPrivateBrowsing] () {
|
||||
observer->ObserveStorageNotification(event, aStorageType, aPrivateBrowsing);
|
||||
});
|
||||
|
||||
if (aImmediateDispatch) {
|
||||
r->Run();
|
||||
} else {
|
||||
nsCOMPtr<nsIEventTarget> et = observer->GetEventTarget();
|
||||
if (et) {
|
||||
et->Dispatch(r.forget());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
StorageNotifierService::Register(StorageNotificationObserver* aObserver)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aObserver);
|
||||
MOZ_ASSERT(!mObservers.Contains(aObserver));
|
||||
|
||||
mObservers.AppendElement(aObserver);
|
||||
}
|
||||
|
||||
void
|
||||
StorageNotifierService::Unregister(StorageNotificationObserver* aObserver)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aObserver);
|
||||
|
||||
// No assertion about mObservers containing aObserver because window calls
|
||||
// this method multiple times.
|
||||
|
||||
mObservers.RemoveElement(aObserver);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
|
@ -1,59 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_StorageNotifierService_h
|
||||
#define mozilla_dom_StorageNotifierService_h
|
||||
|
||||
class nsPIDOMWindowInner;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class StorageEvent;
|
||||
|
||||
class StorageNotificationObserver
|
||||
{
|
||||
public:
|
||||
NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
|
||||
|
||||
virtual void
|
||||
ObserveStorageNotification(StorageEvent* aEvent,
|
||||
const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing) = 0;
|
||||
|
||||
virtual nsIEventTarget*
|
||||
GetEventTarget() const = 0;
|
||||
};
|
||||
|
||||
class StorageNotifierService final
|
||||
{
|
||||
public:
|
||||
NS_INLINE_DECL_REFCOUNTING(StorageNotifierService)
|
||||
|
||||
static StorageNotifierService*
|
||||
GetOrCreate();
|
||||
|
||||
static void
|
||||
Broadcast(StorageEvent* aEvent, const char16_t* aStorageType,
|
||||
bool aPrivateBrowsing, bool aImmediateDispatch);
|
||||
|
||||
void
|
||||
Register(StorageNotificationObserver* aObserver);
|
||||
|
||||
void
|
||||
Unregister(StorageNotificationObserver* aObserver);
|
||||
|
||||
private:
|
||||
StorageNotifierService();
|
||||
~StorageNotifierService();
|
||||
|
||||
nsTObserverArray<RefPtr<StorageNotificationObserver>> mObservers;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_StorageNotifierService_h
|
|
@ -13,7 +13,6 @@ EXPORTS.mozilla.dom += [
|
|||
'SessionStorageManager.h',
|
||||
'Storage.h',
|
||||
'StorageIPC.h',
|
||||
'StorageNotifierService.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
|
@ -27,7 +26,6 @@ UNIFIED_SOURCES += [
|
|||
'StorageDBThread.cpp',
|
||||
'StorageDBUpdater.cpp',
|
||||
'StorageIPC.cpp',
|
||||
'StorageNotifierService.cpp',
|
||||
'StorageObserver.cpp',
|
||||
'StorageUtils.cpp',
|
||||
]
|
||||
|
|
Загрузка…
Ссылка в новой задаче