/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */
Sequence<JSObject*> transferables; // none in this case
JS::Rooted<JS::Value> objValue(aCx, JS::ObjectValue(*obj));
aPort->PostMessage(aCx, objValue, transferables, aRv);
}
// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable // The handler steps of Step 4.
MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
AutoJSAPI jsapi; if (!jsapi.Init(mController->GetParentObject())) { return NS_OK;
}
JSContext* cx = jsapi.cx();
MessageEvent* messageEvent = aEvent->AsMessageEvent(); if (NS_WARN_IF(!messageEvent || !messageEvent->IsTrusted())) { return NS_OK;
}
// Step 1: Let data be the data of the message.
JS::Rooted<JS::Value> dataValue(cx);
IgnoredErrorResult rv;
messageEvent->GetData(cx, &dataValue, rv); if (rv.Failed()) { return NS_OK;
}
// Step 2: Assert: Type(data) is Object. // (But we check in runtime instead to avoid potential malicious events from // a compromised process. Same below.) if (NS_WARN_IF(!dataValue.isObject())) { return NS_OK;
}
JS::Rooted<JSObject*> data(cx, &dataValue.toObject());
// Step 3: Let type be ! Get(data, "type").
JS::Rooted<JS::Value> type(cx); if (!JS_GetProperty(cx, data, "type", &type)) { // XXX: See bug 1762233
JS_ClearPendingException(cx); return NS_OK;
}
// Step 4: Let value be ! Get(data, "value").
JS::Rooted<JS::Value> value(cx); if (!JS_GetProperty(cx, data, "value", &value)) {
JS_ClearPendingException(cx); return NS_OK;
}
// Step 5: Assert: Type(type) is String. if (NS_WARN_IF(!type.isString())) { return NS_OK;
}
// Step 6: If type is "pull", bool equals = false; if (!JS_StringEqualsLiteral(cx, type.toString(), "pull", &equals)) {
JS_ClearPendingException(cx); return NS_OK;
} if (equals) { // Step 6.1: If backpressurePromise is not undefined,
MaybeResolveAndClearBackpressurePromise(); return NS_OK; // implicit
}
// Step 7: If type is "error", if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) {
JS_ClearPendingException(cx); return NS_OK;
} if (equals) { // Step 7.1: Perform ! // WritableStreamDefaultControllerErrorIfNeeded(controller, value).
WritableStreamDefaultControllerErrorIfNeeded(cx, mController, value, rv); if (rv.Failed()) { return NS_OK;
}
// Step 7.2: If backpressurePromise is not undefined,
MaybeResolveAndClearBackpressurePromise(); return NS_OK; // implicit
}
// Logically it should be unreachable here, but we should expect random // malicious messages.
NS_WARNING("Got an unexpected type other than pull/error."); return NS_OK;
}
// mController never changes before CC // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController;
RefPtr<Promise> mBackpressurePromise;
};
// mController never changes before CC // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController;
RefPtr<MessagePort> mPort;
};
// Step 2: If result is an abrupt completion, if (rv.Failed()) { // Step 2.2: Perform ! CrossRealmTransformSendError(port, result.[[Value]]).
MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), aError));
CrossRealmTransformSendError(aCx, aPort, aError); returnfalse;
}
// Step 3: Return result as a completion record. returntrue;
}
// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable class CrossRealmWritableUnderlyingSinkAlgorithms final
: public UnderlyingSinkAlgorithmsBase { public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
CrossRealmWritableUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
void StartCallback(JSContext* aCx,
WritableStreamDefaultController& aController,
JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv) override { // Step 7. Let startAlgorithm be an algorithm that returns undefined.
aRetVal.setUndefined();
}
already_AddRefed<Promise> WriteCallback(
JSContext* aCx, JS::Handle<JS::Value> aChunk,
WritableStreamDefaultController& aController, ErrorResult& aRv) override { // Step 1: If backpressurePromise is undefined, set backpressurePromise to a // promise resolved with undefined. // Note: This promise field is shared with the message event listener. if (!mListener->BackpressurePromise()) {
mListener->CreateBackpressurePromise();
mListener->BackpressurePromise()->MaybeResolveWithUndefined();
}
// Step 2: Return the result of reacting to backpressurePromise with the // following fulfillment steps: auto result =
mListener->BackpressurePromise()->ThenWithCycleCollectedArgsJS(
[](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv,
SetUpTransformWritableMessageEventListener* aListener,
MessagePort* aPort,
JS::Handle<JS::Value> aChunk) -> already_AddRefed<Promise> { // Step 2.1: Set backpressurePromise to a new promise.
aListener->CreateBackpressurePromise();
// Step 2.2: Let result be PackAndPostMessageHandlingError(port, // "chunk", chunk).
JS::Rooted<JS::Value> error(aCx); bool result = PackAndPostMessageHandlingError(
aCx, aPort, u"chunk"_ns, aChunk, &error);
// Step 2.3: If result is an abrupt completion, if (!result) { // Step 2.3.1: Disentangle port. // (Close() does it)
aPort->Close();
// Step 2.3.2: Return a promise rejected with result.[[Value]]. return Promise::CreateRejected(aPort->GetParentObject(), error,
aRv);
}
// Step 2.4: Otherwise, return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(
aPort->GetParentObject(), aRv);
},
std::make_tuple(mListener, mPort), std::make_tuple(aChunk)); if (result.isErr()) {
aRv.Throw(result.unwrapErr()); return nullptr;
} return result.unwrap().forget();
}
already_AddRefed<Promise> CloseCallback(JSContext* aCx,
ErrorResult& aRv) override { // Step 1: Perform ! PackAndPostMessage(port, "close", undefined).
PackAndPostMessage(aCx, mPort, u"close"_ns, JS::UndefinedHandleValue, aRv); // (We'll check the result after step 2)
// Step 2: Disentangle port. // (Close() will do this)
mPort->Close();
if (aRv.Failed()) { return nullptr;
}
// Step 3: Return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
}
already_AddRefed<Promise> AbortCallback(
JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
ErrorResult& aRv) override { // Step 1: Let result be PackAndPostMessageHandlingError(port, "error", // reason).
JS::Rooted<JS::Value> error(aCx); bool result = PackAndPostMessageHandlingError(
aCx, mPort, u"error"_ns,
aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue,
&error);
// Step 2: Disentangle port. // (Close() will do this)
mPort->Close();
// Step 3: If result is an abrupt completion, return a promise rejected with // result.[[Value]]. if (!result) { return Promise::CreateRejected(mPort->GetParentObject(), error, aRv);
}
// Step 4: Otherwise, return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
}
// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable
MOZ_CAN_RUN_SCRIPT staticvoid SetUpCrossRealmTransformWritable(
WritableStream* aWritable, MessagePort* aPort, ErrorResult& aRv) { // (This is only needed for step 12, but let's do this early to fail early // enough)
AutoJSAPI jsapi; if (!jsapi.Init(aWritable->GetParentObject())) { return;
}
JSContext* cx = jsapi.cx();
// Step 1: Perform ! InitializeWritableStream(stream). // (Done by the constructor)
// Step 2: Let controller be a new WritableStreamDefaultController. auto controller = MakeRefPtr<WritableStreamDefaultController>(
aWritable->GetParentObject(), *aWritable);
// Step 3: Let backpressurePromise be a new promise.
RefPtr<Promise> backpressurePromise =
Promise::CreateInfallible(aWritable->GetParentObject());
// Step 4: Add a handler for port’s message event with the following steps: auto listener = MakeRefPtr<SetUpTransformWritableMessageEventListener>(
controller, backpressurePromise);
aPort->AddEventListener(u"message"_ns, listener, false);
// Step 5: Add a handler for port’s messageerror event with the following // steps: auto errorListener =
MakeRefPtr<SetUpTransformWritableMessageErrorEventListener>(controller,
aPort);
aPort->AddEventListener(u"messageerror"_ns, errorListener, false);
// Step 6: Enable port’s port message queue. // (Start() does it)
aPort->Start();
// Step 7 - 10: auto algorithms =
MakeRefPtr<CrossRealmWritableUnderlyingSinkAlgorithms>(listener, aPort);
// Step 11: Let sizeAlgorithm be an algorithm that returns 1. // (nullptr should serve this purpose. See also WritableStream::Constructor)
// Step 1: Let data be the data of the message.
JS::Rooted<JS::Value> dataValue(cx);
IgnoredErrorResult rv;
messageEvent->GetData(cx, &dataValue, rv); if (rv.Failed()) { return NS_OK;
}
// Step 2: Assert: Type(data) is Object. // (But we check in runtime instead to avoid potential malicious events from // a compromised process. Same below.) if (NS_WARN_IF(!dataValue.isObject())) { return NS_OK;
}
JS::Rooted<JSObject*> data(cx, JS::ToObject(cx, dataValue));
// Step 3: Let type be ! Get(data, "type").
JS::Rooted<JS::Value> type(cx); if (!JS_GetProperty(cx, data, "type", &type)) { // XXX: See bug 1762233
JS_ClearPendingException(cx); return NS_OK;
}
// Step 4: Let value be ! Get(data, "value").
JS::Rooted<JS::Value> value(cx); if (!JS_GetProperty(cx, data, "value", &value)) {
JS_ClearPendingException(cx); return NS_OK;
}
// Step 5: Assert: Type(type) is String. if (NS_WARN_IF(!type.isString())) { return NS_OK;
}
// Step 6: If type is "chunk", bool equals = false; if (!JS_StringEqualsLiteral(cx, type.toString(), "chunk", &equals)) {
JS_ClearPendingException(cx); return NS_OK;
} if (equals) { // Step 6.1: Perform ! ReadableStreamDefaultControllerEnqueue(controller, // value).
ReadableStreamDefaultControllerEnqueue(cx, mController, value,
IgnoreErrors());
cleanupPort.release(); return NS_OK; // implicit
}
// Step 7: Otherwise, if type is "close", if (!JS_StringEqualsLiteral(cx, type.toString(), "close", &equals)) {
JS_ClearPendingException(cx); return NS_OK;
} if (equals) { // Step 7.1: Perform ! ReadableStreamDefaultControllerClose(controller).
ReadableStreamDefaultControllerClose(cx, mController, IgnoreErrors()); // Step 7.2: Disentangle port. // (Close() does it)
mPort->Close();
cleanupPort.release(); return NS_OK; // implicit
}
// Step 8: Otherwise, if type is "error", if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) {
JS_ClearPendingException(cx); return NS_OK;
} if (equals) { // Step 8.1: Perform ! ReadableStreamDefaultControllerError(controller, // value).
ReadableStreamDefaultControllerError(cx, mController, value,
IgnoreErrors());
// Logically it should be unreachable here, but we should expect random // malicious messages.
NS_WARNING("Got an unexpected type other than chunk/close/error."); return NS_OK;
}
// mController never changes before CC // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
MOZ_KNOWN_LIVE RefPtr<ReadableStreamDefaultController> mController;
RefPtr<MessagePort> mPort;
};
// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable class CrossRealmReadableUnderlyingSourceAlgorithms final
: public UnderlyingSourceAlgorithmsBase { public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
CrossRealmReadableUnderlyingSourceAlgorithms,
UnderlyingSourceAlgorithmsBase)
void StartCallback(JSContext* aCx, ReadableStreamController& aController,
JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv) override { // Step 6. Let startAlgorithm be an algorithm that returns undefined.
aRetVal.setUndefined();
}
already_AddRefed<Promise> PullCallback(JSContext* aCx,
ReadableStreamController& aController,
ErrorResult& aRv) override { // Step 7: Let pullAlgorithm be the following steps:
// Step 7.2: Return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
}
already_AddRefed<Promise> CancelCallback(
JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
ErrorResult& aRv) override { // Step 8: Let cancelAlgorithm be the following steps, taking a reason // argument:
// Step 8.1: Let result be PackAndPostMessageHandlingError(port, "error", // reason).
JS::Rooted<JS::Value> error(aCx); bool result = PackAndPostMessageHandlingError(
aCx, mPort, u"error"_ns,
aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue,
&error);
// Step 8.2: Disentangle port. // (Close() does it)
mPort->Close();
// Step 8.3: If result is an abrupt completion, return a promise rejected // with result.[[Value]]. if (!result) { return Promise::CreateRejected(mPort->GetParentObject(), error, aRv);
}
// Step 8.4: Otherwise, return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
}
// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
MOZ_CAN_RUN_SCRIPT staticvoid SetUpCrossRealmTransformReadable(
ReadableStream* aReadable, MessagePort* aPort, ErrorResult& aRv) { // (This is only needed for step 10, but let's do this early to fail early // enough)
AutoJSAPI jsapi; if (!jsapi.Init(aReadable->GetParentObject())) { return;
}
JSContext* cx = jsapi.cx();
// Step 1: Perform ! InitializeReadableStream(stream). // (This is implicitly done by the constructor)
// Step 2: Let controller be a new ReadableStreamDefaultController. auto controller =
MakeRefPtr<ReadableStreamDefaultController>(aReadable->GetParentObject());
// Step 3: Add a handler for port’s message event with the following steps: auto listener =
MakeRefPtr<SetUpTransformReadableMessageEventListener>(controller, aPort);
aPort->AddEventListener(u"message"_ns, listener, false);
// Step 4: Add a handler for port’s messageerror event with the following // steps: auto errorListener =
MakeRefPtr<SetUpTransformReadableMessageErrorEventListener>(controller,
aPort);
aPort->AddEventListener(u"messageerror"_ns, errorListener, false);
// Step 5: Enable port’s port message queue. // (Start() does it)
aPort->Start();
// Step 6-8: auto algorithms =
MakeRefPtr<CrossRealmReadableUnderlyingSourceAlgorithms>(aPort);
// Step 9: Let sizeAlgorithm be an algorithm that returns 1. // (nullptr should serve this purpose. See also ReadableStream::Constructor)
// https://streams.spec.whatwg.org/#ref-for-transfer-steps bool ReadableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) { // Step 1: If ! IsReadableStreamLocked(value) is true, throw a // "DataCloneError" DOMException. // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double // check here as the state might have changed in case this ReadableStream is // created by a TransferStream and being transferred together with the // parent.) if (IsReadableStreamLocked(this)) { returnfalse;
}
// Step 2: Let port1 be a new MessagePort in the current Realm. // Step 3: Let port2 be a new MessagePort in the current Realm. // Step 4: Entangle port1 and port2. // (The MessageChannel constructor does exactly that.) // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel
ErrorResult rv;
RefPtr<dom::MessageChannel> channel =
dom::MessageChannel::Constructor(mGlobal, rv); if (rv.MaybeSetPendingException(aCx)) { returnfalse;
}
// Step 5: Let writable be a new WritableStream in the current Realm.
RefPtr<WritableStream> writable = new WritableStream(
mGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit);
// Step 6: Perform ! SetUpCrossRealmTransformWritable(writable, port1). // MOZ_KnownLive because Port1 never changes before CC
SetUpCrossRealmTransformWritable(writable, MOZ_KnownLive(channel->Port1()),
rv); if (rv.MaybeSetPendingException(aCx)) { returnfalse;
}
// Step 7: Let promise be ! ReadableStreamPipeTo(value, writable, false, // false, false).
RefPtr<Promise> promise =
ReadableStreamPipeTo(this, writable, false, false, false, nullptr, rv); if (rv.MaybeSetPendingException(aCx)) { returnfalse;
}
// Step 8: Set promise.[[PromiseIsHandled]] to true.
MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled());
// Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2, // « port2 »).
channel->Port2()->CloneAndDisentangle(aPortId);
returntrue;
}
// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps
MOZ_CAN_RUN_SCRIPT already_AddRefed<ReadableStream>
ReadableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal,
MessagePort& aPort) { // Step 1: Let deserializedRecord be // ! StructuredDeserializeWithTransfer(dataHolder.[[port]], the current // Realm). // Step 2: Let port be deserializedRecord.[[Deserialized]].
// https://streams.spec.whatwg.org/#ref-for-transfer-steps① bool WritableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) { // Step 1: If ! IsWritableStreamLocked(value) is true, throw a // "DataCloneError" DOMException. // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double // check here as the state might have changed in case this WritableStream is // created by a TransferStream and being transferred together with the // parent.) if (IsWritableStreamLocked(this)) { returnfalse;
}
// Step 2: Let port1 be a new MessagePort in the current Realm. // Step 3: Let port2 be a new MessagePort in the current Realm. // Step 4: Entangle port1 and port2. // (The MessageChannel constructor does exactly that.) // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel
ErrorResult rv;
RefPtr<dom::MessageChannel> channel =
dom::MessageChannel::Constructor(mGlobal, rv); if (rv.MaybeSetPendingException(aCx)) { returnfalse;
}
// Step 5: Let readable be a new ReadableStream in the current Realm.
RefPtr<ReadableStream> readable = new ReadableStream(
mGlobal, ReadableStream::HoldDropJSObjectsCaller::Implicit);
// Step 6: Perform ! SetUpCrossRealmTransformReadable(readable, port1). // MOZ_KnownLive because Port1 never changes before CC
SetUpCrossRealmTransformReadable(readable, MOZ_KnownLive(channel->Port1()),
rv); if (rv.MaybeSetPendingException(aCx)) { returnfalse;
}
// Step 7: Let promise be ! ReadableStreamPipeTo(readable, value, false, // false, false).
RefPtr<Promise> promise =
ReadableStreamPipeTo(readable, this, false, false, false, nullptr, rv); if (rv.Failed()) { returnfalse;
}
// Step 8: Set promise.[[PromiseIsHandled]] to true.
MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled());
// Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2, // « port2 »).
channel->Port2()->CloneAndDisentangle(aPortId);
returntrue;
}
// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps①
MOZ_CAN_RUN_SCRIPT already_AddRefed<WritableStream>
WritableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal,
MessagePort& aPort) { // Step 1: Let deserializedRecord be ! // StructuredDeserializeWithTransfer(dataHolder.[[port]], the current Realm). // Step 2: Let port be a deserializedRecord.[[Deserialized]].
// https://streams.spec.whatwg.org/#ref-for-transfer-steps② bool TransformStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId1,
UniqueMessagePortId& aPortId2) { // Step 1: Let readable be value.[[readable]]. // Step 2: Let writable be value.[[writable]]. // Step 3: If ! IsReadableStreamLocked(readable) is true, throw a // "DataCloneError" DOMException. // Step 4: If ! IsWritableStreamLocked(writable) is true, throw a // "DataCloneError" DOMException. // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double // check here as the state might have changed by // Readable/WritableStream::Transfer in case the stream members of this // TransformStream are being transferred together.) if (IsReadableStreamLocked(mReadable) || IsWritableStreamLocked(mWritable)) { returnfalse;
}
// Step 5: Set dataHolder.[[readable]] to ! // StructuredSerializeWithTransfer(readable, « readable »). // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854) if (!MOZ_KnownLive(mReadable)->Transfer(aCx, aPortId1)) { returnfalse;
}
// Step 6: Set dataHolder.[[writable]] to ! // StructuredSerializeWithTransfer(writable, « writable »). // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854) return MOZ_KnownLive(mWritable)->Transfer(aCx, aPortId2);
}
// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps② bool TransformStream::ReceiveTransfer(
JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort1,
MessagePort& aPort2, JS::MutableHandle<JSObject*> aReturnObject) { // Step 1: Let readableRecord be ! // StructuredDeserializeWithTransfer(dataHolder.[[readable]], the current // Realm).
RefPtr<ReadableStream> readable =
ReadableStream::ReceiveTransferImpl(aCx, aGlobal, aPort1); if (!readable) { returnfalse;
}
// Step 2: Let writableRecord be ! // StructuredDeserializeWithTransfer(dataHolder.[[writable]], the current // Realm).
RefPtr<WritableStream> writable =
WritableStream::ReceiveTransferImpl(aCx, aGlobal, aPort2); if (!writable) { returnfalse;
}
// Step 3: Set value.[[readable]] to readableRecord.[[Deserialized]]. // Step 4: Set value.[[writable]] to writableRecord.[[Deserialized]]. // Step 5: Set value.[[backpressure]], value.[[backpressureChangePromise]], // and value.[[controller]] to undefined.
RefPtr<TransformStream> stream = new TransformStream(aGlobal, readable, writable);
JS::Rooted<JS::Value> value(aCx); if (!GetOrCreateDOMReflector(aCx, stream, &value)) {
JS_ClearPendingException(aCx); returnfalse;
}
aReturnObject.set(&value.toObject());
returntrue;
}
} // namespace mozilla::dom
¤ Dauer der Verarbeitung: 0.26 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.