Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Web Inspector: Support ES2022 Private Fields
https://bugs.webkit.org/show_bug.cgi?id=254961
<rdar://problem/107863310>

Reviewed by Patrick Angle and Justin Michaud.

Web Inspector should show private fields when logging objects in the Console.

As private field usage becomes more and more popular, not being able to see private fields in the Console will become increasingly disruptive to the debugging experience.

* Source/JavaScriptCore/inspector/InjectedScriptSource.js:
(InjectedScript.prototype._forEachPropertyDescriptor):
(InjectedScript.prototype._forEachPropertyDescriptor.processProperty):
* Source/JavaScriptCore/inspector/JSInjectedScriptHost.h:
* Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp:
(Inspector::JSInjectedScriptHost::getOwnPrivatePropertyDescriptors): Added.
* Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp:
(Inspector::JSInjectedScriptHostPrototype::finishCreation):
(Inspector::jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertyDescriptors): Added.
Create a `InjectedScriptHost` method similar to `Object.getOwnPropertyDescriptors` but only for private fields.
Use it when fetching property descriptors for the frontend to grab private fields just like all other public fields.

* Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp:
(JSC::PropertyListNode::emitDeclarePrivateFieldNames):
Pass along the `PropertyNode::name` when creating a private symbol for the private field.

* Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp:
(JSC::BytecodeGenerator::emitCreatePrivateBrand):
AFAIK there is no name in this scenario, just a private symbol to mark with a private brand, so use an empty string for the name.

* Source/JavaScriptCore/runtime/JSGlobalObject.cpp:
(JSC::createPrivateSymbol):
(JSC::JSGlobalObject::init):
Require a name when calling `@createPrivateSymbol`.

* Source/JavaScriptCore/runtime/PrivateName.h:
(JSC::PrivateName::PrivateName): Deleted.
* Source/JavaScriptCore/runtime/Symbol.cpp:
(JSC::Symbol::Symbol):
* Source/WTF/wtf/text/SymbolImpl.h:
* Source/WTF/wtf/text/SymbolImpl.cpp:
(WTF::PrivateSymbolImpl::createNullSymbol): Deleted.
* Source/WebKit/WebProcess/WebPage/IPCTestingAPI.cpp:
(WebKit::IPCTestingAPI::JSMessageListener::JSMessageListener):
Require that all `PrivateName` and `PrivateSymbolImpl` be created with a name (or at least an empty string).

* Source/JavaScriptCore/inspector/protocol/Runtime.json:
* Source/WebInspectorUI/UserInterface/Models/PropertyDescriptor.js:
(WI.PropertyDescriptor):
(WI.PropertyDescriptor.fromPayload):
(WI.PropertyDescriptor.get isPrivateProperty): Added.
Expose a way for clients to know that this `WI.PropertyDescriptor` is for a private field.

* Source/WebInspectorUI/UserInterface/Models/PropertyPath.js:
(WI.PropertyPath.prototype.appendPrivatePropertyName): Added.
(WI.PropertyPath.prototype.appendPropertyDescriptor):
Make sure that private fields are sufficiently hidden (i.e. do not show "Copy Path to Property" since it won't work).

* Source/WebInspectorUI/UserInterface/Views/ObjectTreeView.js:
(WI.ObjectTreeView.comparePropertyDescriptors):
Sort private fields above all other non-internal properties.

* Source/WebInspectorUI/UserInterface/Views/ObjectTreePropertyTreeElement.js:
(WI.ObjectTreePropertyTreeElement.prototype._createTitlePropertyStyle):
* Source/WebInspectorUI/UserInterface/Views/ObjectTreePropertyTreeElement.css:
(.object-tree-property .property-name:is(.private, .not-enumerable)): Renamed from `.object-tree-property .property-name.not-enumerable`.
Display private fields as a greyed out property (similar to internal properties) in order to help emphasize their semi-unreachable nature.

* LayoutTests/inspector/runtime/getDisplayableProperties.html:
* LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt:
* LayoutTests/inspector/runtime/getProperties.html:
* LayoutTests/inspector/runtime/getProperties-expected.txt:
* LayoutTests/inspector/runtime/resources/property-descriptor-utilities.js:
(ProtocolTest.PropertyDescriptorUtilities.logForEach):

Canonical link: https://commits.webkit.org/262882@main
  • Loading branch information
dcrousso committed Apr 12, 2023
1 parent 893e62e commit 444a3dc
Show file tree
Hide file tree
Showing 23 changed files with 291 additions and 31 deletions.
Expand Up @@ -132,6 +132,51 @@ Internal Properties:
"boundThis" => null (object null) []
"boundArgs" => "Array" (object array) []

-- Running test case: Runtime.getDisplayableProperties.Private.Instance
Evaluating expression...
Getting displayable properties...
Properties:
"#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn]

-- Running test case: Runtime.getDisplayableProperties.Private.Constructor
Evaluating expression...
Getting displayable properties...
Properties:
"#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"length" => 0 (number) [configurable | isOwn]
"prototype" => "PrivateMembersTestClassChild" (object) [isOwn]
"childClassPublicMethod" => "function childClassPublicMethod() { }" (function) [writable | configurable | isOwn]
"childClassPublicGetter" => get "function () { }" (function) [configurable | isOwn]
"childClassPublicGetter" => set undefined (undefined) [configurable | isOwn]
"childClassPublicSetter" => get undefined (undefined) [configurable | isOwn]
"childClassPublicSetter" => set "function (x) { }" (function) [configurable | isOwn]
"childClassPublicGetterSetter" => get "function () { }" (function) [configurable | isOwn]
"childClassPublicGetterSetter" => set "function (x) { }" (function) [configurable | isOwn]
"toString" => "function toString() { return \"<redacted>\"; }" (function) [writable | configurable | isOwn]
"childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn]
"arguments" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown]
"caller" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown]
"__proto__" => "<redacted>" (function class) [writable | configurable | isOwn]

-- Running test case: Runtime.getDisplayableProperties.Private.Prototype
Evaluating expression...
Getting displayable properties...
Properties:
"constructor" => "<redacted>" (function class) [writable | configurable | isOwn]
"childInstancePublicMethod" => "function childInstancePublicMethod() { }" (function) [writable | configurable | isOwn]
"childInstancePublicGetter" => get "function () { }" (function) [configurable | isOwn]
"childInstancePublicGetter" => set undefined (undefined) [configurable | isOwn]
"childInstancePublicSetter" => get undefined (undefined) [configurable | isOwn]
"childInstancePublicSetter" => set "function (x) { }" (function) [configurable | isOwn]
"childInstancePublicGetterSetter" => get "function () { }" (function) [configurable | isOwn]
"childInstancePublicGetterSetter" => set "function (x) { }" (function) [configurable | isOwn]
"__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn]

-- Running test case: Runtime.getDisplayableProperties.Promise.Resolved
Evaluating expression...
Getting displayable properties...
Expand Down
18 changes: 18 additions & 0 deletions LayoutTests/inspector/runtime/getDisplayableProperties.html
Expand Up @@ -109,6 +109,24 @@
expression: `(function(){}).bind(null, 1, 2, 3)`,
});

addTestCase({
name: "Runtime.getDisplayableProperties.Private.Instance",
expression: `new PrivateMembersTestClassChild`,
ownProperties: true,
});

addTestCase({
name: "Runtime.getDisplayableProperties.Private.Constructor",
expression: `PrivateMembersTestClassChild`,
ownProperties: true,
});

addTestCase({
name: "Runtime.getDisplayableProperties.Private.Prototype",
expression: `PrivateMembersTestClassChild.prototype`,
ownProperties: true,
});

addTestCase({
name: "Runtime.getDisplayableProperties.Promise.Resolved",
expression: `Promise.resolve(123)`,
Expand Down
43 changes: 43 additions & 0 deletions LayoutTests/inspector/runtime/getProperties-expected.txt
Expand Up @@ -114,6 +114,49 @@ Internal Properties:
"boundThis" => null (object null) []
"boundArgs" => "Array" (object array) []

-- Running test case: Runtime.getProperties.Private.Instance
Evaluating expression...
Getting own properties...
Properties:
"#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn]

-- Running test case: Runtime.getProperties.Private.Constructor
Evaluating expression...
Getting own properties...
Properties:
"#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate]
"length" => 0 (number) [configurable | isOwn]
"prototype" => "PrivateMembersTestClassChild" (object) [isOwn]
"childClassPublicMethod" => "function childClassPublicMethod() { }" (function) [writable | configurable | isOwn]
"childClassPublicGetter" => get "function () { }" (function) [configurable | isOwn]
"childClassPublicGetter" => set undefined (undefined) [configurable | isOwn]
"childClassPublicSetter" => get undefined (undefined) [configurable | isOwn]
"childClassPublicSetter" => set "function (x) { }" (function) [configurable | isOwn]
"childClassPublicGetterSetter" => get "function () { }" (function) [configurable | isOwn]
"childClassPublicGetterSetter" => set "function (x) { }" (function) [configurable | isOwn]
"toString" => "function toString() { return \"<redacted>\"; }" (function) [writable | configurable | isOwn]
"childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn]
"name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn]
"__proto__" => "<redacted>" (function class) [writable | configurable | isOwn]

-- Running test case: Runtime.getProperties.Private.Prototype
Evaluating expression...
Getting own properties...
Properties:
"constructor" => "<redacted>" (function class) [writable | configurable | isOwn]
"childInstancePublicMethod" => "function childInstancePublicMethod() { }" (function) [writable | configurable | isOwn]
"childInstancePublicGetter" => get "function () { }" (function) [configurable | isOwn]
"childInstancePublicGetter" => set undefined (undefined) [configurable | isOwn]
"childInstancePublicSetter" => get undefined (undefined) [configurable | isOwn]
"childInstancePublicSetter" => set "function (x) { }" (function) [configurable | isOwn]
"childInstancePublicGetterSetter" => get "function () { }" (function) [configurable | isOwn]
"childInstancePublicGetterSetter" => set "function (x) { }" (function) [configurable | isOwn]
"__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn]

-- Running test case: Runtime.getProperties.Promise.Resolved
Evaluating expression...
Getting own properties...
Expand Down
18 changes: 18 additions & 0 deletions LayoutTests/inspector/runtime/getProperties.html
Expand Up @@ -124,6 +124,24 @@
ownProperties: true,
});

addTestCase({
name: "Runtime.getProperties.Private.Instance",
expression: `new PrivateMembersTestClassChild`.replaceAll(/\s+/g, ' '),
ownProperties: true,
});

addTestCase({
name: "Runtime.getProperties.Private.Constructor",
expression: `PrivateMembersTestClassChild`.replaceAll(/\s+/g, ' '),
ownProperties: true,
});

addTestCase({
name: "Runtime.getProperties.Private.Prototype",
expression: `PrivateMembersTestClassChild.prototype`.replaceAll(/\s+/g, ' '),
ownProperties: true,
});

addTestCase({
name: "Runtime.getProperties.Promise.Resolved",
expression: `Promise.resolve(123)`,
Expand Down
@@ -1,3 +1,59 @@
class PrivateMembersTestClassParent {
parentInstancePublicProperty = 'parentInstancePublicPropertyValue';
#parentInstancePrivateProperty = 'parentInstancePrivatePropertyValue';
parentInstancePublicMethod() { }
#parentInstancePrivateMethod() { }
get parentInstancePublicGetter() { }
set parentInstancePublicSetter(x) { }
get parentInstancePublicGetterSetter() { }
set parentInstancePublicGetterSetter(x) { }
get #parentInstancePrivateGetter() { }
set #parentInstancePrivateSetter(x) { }
get #parentInstancePrivateGetterSetter() { }
set #parentInstancePrivateGetterSetter(x) { }
static parentClassPublicProperty = 'parentClassPublicPropertyValue';
static #parentClassPrivateProperty = 'parentClassPrivatePropertyValue';
static parentClassPublicMethod() { }
static #parentClassPrivateMethod() { }
static get parentClassPublicGetter() { }
static set parentClassPublicSetter(x) { }
static get parentClassPublicGetterSetter() { }
static set parentClassPublicGetterSetter(x) { }
static get #parentClassPrivateGetter() { }
static set #parentClassPrivateSetter(x) { }
static get #parentClassPrivateGetterSetter() { }
static set #parentClassPrivateGetterSetter(x) { }
static toString() { return "<redacted>"; }
}

class PrivateMembersTestClassChild extends PrivateMembersTestClassParent {
childInstancePublicProperty = 'childInstancePublicPropertyValue';
#childInstancePrivateProperty = 'childInstancePrivatePropertyValue';
childInstancePublicMethod() { }
#childInstancePrivateMethod() { }
get childInstancePublicGetter() { }
set childInstancePublicSetter(x) { }
get childInstancePublicGetterSetter() { }
set childInstancePublicGetterSetter(x) { }
get #childInstancePrivateGetter() { }
set #childInstancePrivateSetter(x) { }
get #childInstancePrivateGetterSetter() { }
set #childInstancePrivateGetterSetter(x) { }
static childClassPublicProperty = 'childClassPublicPropertyValue';
static #childClassPrivateProperty = 'childClassPrivatePropertyValue';
static childClassPublicMethod() { }
static #childClassPrivateMethod() { }
static get childClassPublicGetter() { }
static set childClassPublicSetter(x) { }
static get childClassPublicGetterSetter() { }
static set childClassPublicGetterSetter(x) { }
static get #childClassPrivateGetter() { }
static set #childClassPrivateSetter(x) { }
static get #childClassPrivateGetterSetter() { }
static set #childClassPrivateGetterSetter(x) { }
static toString() { return "<redacted>"; }
}

function makeArray(length) {
return Array(length).fill().map((item, i) => {
let code = (i % 2) ? ("Z".charCodeAt(0) - i) : ("A".charCodeAt(0) + i);
Expand Down Expand Up @@ -32,18 +88,23 @@ function makeWeakSet(length) {
TestPage.registerInitializer(() => {
ProtocolTest.PropertyDescriptorUtilities = {};

ProtocolTest.PropertyDescriptorUtilities.logForEach = function({name, value, ...extra}, index, array) {
ProtocolTest.PropertyDescriptorUtilities.logForEach = function({name, value, get, set, ...extra}, index, array) {
let maxPropertyNameLength = array.reduce((max, {name}) => name.length > max ? name.length : max, 0) + 2; // add 2 for the surrounding quotes
let paddedName = JSON.stringify(name).padEnd(maxPropertyNameLength, " ");
let descriptorKeys = [];
for (let [key, value] of Object.entries(extra)) {
if (value) {
ProtocolTest.assert(typeof value === "boolean", `Property descriptor '${key}' has a non-boolean value '${value}'.`);
ProtocolTest.assert(typeof value === "boolean", `Property descriptor '${key}' has a non-boolean value '${JSON.stringify(value)}'.`);
descriptorKeys.push(key);
}
}

ProtocolTest.log(` ${paddedName} => ${ProtocolTest.PropertyDescriptorUtilities.stringifyRemoteObject(value)} [${descriptorKeys.join(" | ")}]`);
if (value)
ProtocolTest.log(` ${paddedName} => ${ProtocolTest.PropertyDescriptorUtilities.stringifyRemoteObject(value)} [${descriptorKeys.join(" | ")}]`);
if (get)
ProtocolTest.log(` ${paddedName} => get ${ProtocolTest.PropertyDescriptorUtilities.stringifyRemoteObject(get)} [${descriptorKeys.join(" | ")}]`);
if (set)
ProtocolTest.log(` ${paddedName} => set ${ProtocolTest.PropertyDescriptorUtilities.stringifyRemoteObject(set)} [${descriptorKeys.join(" | ")}]`);
};

ProtocolTest.PropertyDescriptorUtilities.stringifyRemoteObject = function(remoteObject) {
Expand Down
3 changes: 2 additions & 1 deletion Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
Expand Up @@ -2848,8 +2848,9 @@ void BytecodeGenerator::emitCreatePrivateBrand(const JSTextPosition& divot, cons
{
RefPtr<RegisterID> createPrivateSymbol = moveLinkTimeConstant(nullptr, LinkTimeConstant::createPrivateSymbol);

CallArguments arguments(*this, nullptr, 0);
CallArguments arguments(*this, nullptr, 1);
emitLoad(arguments.thisRegister(), jsUndefined());
emitLoad(arguments.argumentRegister(0), jsEmptyString(m_vm));
RegisterID* newSymbol = emitCall(finalDestination(nullptr, createPrivateSymbol.get()), createPrivateSymbol.get(), NoExpectedFunction, arguments, divot, divotStart, divotEnd, DebuggableCall::No);

Variable privateBrandVar = variable(propertyNames().builtinNames().privateBrandPrivateName());
Expand Down
3 changes: 2 additions & 1 deletion Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp
Expand Up @@ -572,8 +572,9 @@ void PropertyListNode::emitDeclarePrivateFieldNames(BytecodeGenerator& generator
if (!createPrivateSymbol)
createPrivateSymbol = generator.moveLinkTimeConstant(nullptr, LinkTimeConstant::createPrivateSymbol);

CallArguments arguments(generator, nullptr, 0);
CallArguments arguments(generator, nullptr, 1);
generator.emitLoad(arguments.thisRegister(), jsUndefined());
generator.emitLoad(arguments.argumentRegister(0), *node.name());
RefPtr<RegisterID> symbol = generator.emitCall(generator.finalDestination(nullptr, createPrivateSymbol.get()), createPrivateSymbol.get(), NoExpectedFunction, arguments, position(), position(), position(), DebuggableCall::No);

Variable var = generator.variable(*node.name());
Expand Down
19 changes: 17 additions & 2 deletions Source/JavaScriptCore/inspector/InjectedScriptSource.js
Expand Up @@ -768,7 +768,7 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase
}
}

function processProperty(o, propertyName, isOwnProperty)
function processProperty(o, propertyName, isOwnProperty, privateDescriptor)
{
if (nameProcessed.@has(propertyName))
return InjectedScript.PropertyFetchAction.Continue;
Expand All @@ -778,7 +778,7 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase
let name = toString(propertyName);
let symbol = isSymbol(propertyName) ? propertyName : null;

let descriptor = @Object.@getOwnPropertyDescriptor(o, propertyName);
let descriptor = privateDescriptor || @Object.@getOwnPropertyDescriptor(o, propertyName);
if (!descriptor) {
// FIXME: Bad descriptor. Can we get here?
// Fall back to very restrictive settings.
Expand All @@ -800,6 +800,8 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase
descriptor.isOwn = true;
if (symbol)
descriptor.symbol = symbol;
if (privateDescriptor)
descriptor.isPrivate = true;
return processDescriptor(descriptor, isOwnProperty);
}

Expand All @@ -812,6 +814,19 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase
let isOwnProperty = o === object;
let shouldBreak = false;

let privatePropertyDescriptors = InjectedScriptHost.getOwnPrivatePropertyDescriptors(o);
let privatePropertyNames = @Object.@getOwnPropertyNames(privatePropertyDescriptors);
for (let i = 0; i < privatePropertyNames.length; ++i) {
let privatePropertyName = privatePropertyNames[i];
let result = processProperty(o, privatePropertyName, isOwnProperty, privatePropertyDescriptors[privatePropertyName]);
shouldBreak = result === InjectedScript.PropertyFetchAction.Stop;
if (shouldBreak)
break;
}

if (shouldBreak)
break;

// FIXME: <https://webkit.org/b/201861> Web Inspector: show autocomplete entries for non-index properties on arrays
if (isArrayLike && isOwnProperty) {
for (let i = 0; i < o.length; ++i) {
Expand Down
34 changes: 34 additions & 0 deletions Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp
Expand Up @@ -31,6 +31,7 @@
#include "DateInstance.h"
#include "DeferGCInlines.h"
#include "DirectArguments.h"
#include "EnumerationMode.h"
#include "FunctionPrototype.h"
#include "HeapAnalyzer.h"
#include "HeapIterationScope.h"
Expand All @@ -56,6 +57,7 @@
#include "MarkedSpaceInlines.h"
#include "ObjectConstructor.h"
#include "PreventCollectionScope.h"
#include "PropertyNameArray.h"
#include "ProxyObject.h"
#include "RegExpObject.h"
#include "ScopedArguments.h"
Expand Down Expand Up @@ -293,6 +295,38 @@ static JSObject* constructInternalProperty(JSGlobalObject* globalObject, const S
return result;
}

JSValue JSInjectedScriptHost::getOwnPrivatePropertyDescriptors(JSGlobalObject* globalObject, CallFrame* callFrame)
{
if (callFrame->argumentCount() < 1)
return jsUndefined();

VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue value = callFrame->uncheckedArgument(0);

JSObject* result = constructEmptyObject(globalObject);
RETURN_IF_EXCEPTION(scope, JSValue());

JSObject* object = jsDynamicCast<JSObject*>(value);
if (!object)
return result;

PropertyNameArray propertyNames(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include);
JSObject::getOwnPropertyNames(object, globalObject, propertyNames, DontEnumPropertiesMode::Include);
for (const auto& propertyName : propertyNames) {
if (!propertyName.isPrivateName())
continue;

// Authored private properties are indistinguishable from internal private properties except for their use of the `#` prefix.
if (!propertyName.string().startsWith('#'))
continue;

result->putDirect(vm, Identifier::fromString(vm, String(propertyName.impl()->isolatedCopy())), objectConstructorGetOwnPropertyDescriptor(globalObject, object, propertyName));
}

return result;
}

JSValue JSInjectedScriptHost::getInternalProperties(JSGlobalObject* globalObject, CallFrame* callFrame)
{
if (callFrame->argumentCount() < 1)
Expand Down

0 comments on commit 444a3dc

Please sign in to comment.