| /* |
| * Copyright (C) 2015-2019 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "JSCustomElementRegistry.h" |
| |
| #include "CustomElementRegistry.h" |
| #include "Document.h" |
| #include "HTMLNames.h" |
| #include "JSCustomElementInterface.h" |
| #include "JSDOMBinding.h" |
| #include "JSDOMConvertSequences.h" |
| #include "JSDOMConvertStrings.h" |
| #include "JSDOMPromiseDeferred.h" |
| #include <wtf/SetForScope.h> |
| |
| |
| namespace WebCore { |
| using namespace JSC; |
| |
| static JSObject* getCustomElementCallback(ExecState& state, JSObject& prototype, const Identifier& id) |
| { |
| VM& vm = state.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSValue callback = prototype.get(&state, id); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| if (callback.isUndefined()) |
| return nullptr; |
| if (!callback.isFunction(vm)) { |
| throwTypeError(&state, scope, "A custom element callback must be a function"_s); |
| return nullptr; |
| } |
| return callback.getObject(); |
| } |
| |
| static bool validateCustomElementNameAndThrowIfNeeded(ExecState& state, const AtomString& name) |
| { |
| auto scope = DECLARE_THROW_SCOPE(state.vm()); |
| switch (Document::validateCustomElementName(name)) { |
| case CustomElementNameValidationStatus::Valid: |
| return true; |
| case CustomElementNameValidationStatus::FirstCharacterIsNotLowercaseASCIILetter: |
| throwDOMSyntaxError(state, scope, "Custom element name must have a lowercase ASCII letter as its first character"_s); |
| return false; |
| case CustomElementNameValidationStatus::ContainsUppercaseASCIILetter: |
| throwDOMSyntaxError(state, scope, "Custom element name cannot contain an uppercase ASCII letter"_s); |
| return false; |
| case CustomElementNameValidationStatus::ContainsNoHyphen: |
| throwDOMSyntaxError(state, scope, "Custom element name must contain a hyphen"_s); |
| return false; |
| case CustomElementNameValidationStatus::ContainsDisallowedCharacter: |
| throwDOMSyntaxError(state, scope, "Custom element name contains a character that is not allowed"_s); |
| return false; |
| case CustomElementNameValidationStatus::ConflictsWithStandardElementName: |
| throwDOMSyntaxError(state, scope, "Custom element name cannot be same as one of the standard elements"_s); |
| return false; |
| } |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| // https://html.spec.whatwg.org/#dom-customelementregistry-define |
| JSValue JSCustomElementRegistry::define(ExecState& state) |
| { |
| VM& vm = state.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (UNLIKELY(state.argumentCount() < 2)) |
| return throwException(&state, scope, createNotEnoughArgumentsError(&state)); |
| |
| AtomString localName(state.uncheckedArgument(0).toString(&state)->toAtomString(&state)); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| |
| JSValue constructorValue = state.uncheckedArgument(1); |
| if (!constructorValue.isConstructor(vm)) |
| return throwTypeError(&state, scope, "The second argument must be a constructor"_s); |
| JSObject* constructor = constructorValue.getObject(); |
| |
| if (!validateCustomElementNameAndThrowIfNeeded(state, localName)) |
| return jsUndefined(); |
| |
| CustomElementRegistry& registry = wrapped(); |
| |
| if (registry.elementDefinitionIsRunning()) { |
| throwNotSupportedError(state, scope, "Cannot define a custom element while defining another custom element"_s); |
| return jsUndefined(); |
| } |
| SetForScope<bool> change(registry.elementDefinitionIsRunning(), true); |
| |
| if (registry.findInterface(localName)) { |
| throwNotSupportedError(state, scope, "Cannot define multiple custom elements with the same tag name"_s); |
| return jsUndefined(); |
| } |
| |
| if (registry.containsConstructor(constructor)) { |
| throwNotSupportedError(state, scope, "Cannot define multiple custom elements with the same class"_s); |
| return jsUndefined(); |
| } |
| |
| JSValue prototypeValue = constructor->get(&state, vm.propertyNames->prototype); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| if (!prototypeValue.isObject()) |
| return throwTypeError(&state, scope, "Custom element constructor's prototype must be an object"_s); |
| JSObject& prototypeObject = *asObject(prototypeValue); |
| |
| QualifiedName name(nullAtom(), localName, HTMLNames::xhtmlNamespaceURI); |
| auto elementInterface = JSCustomElementInterface::create(name, constructor, globalObject()); |
| |
| auto* connectedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(vm, "connectedCallback")); |
| if (connectedCallback) |
| elementInterface->setConnectedCallback(connectedCallback); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| |
| auto* disconnectedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(vm, "disconnectedCallback")); |
| if (disconnectedCallback) |
| elementInterface->setDisconnectedCallback(disconnectedCallback); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| |
| auto* adoptedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(vm, "adoptedCallback")); |
| if (adoptedCallback) |
| elementInterface->setAdoptedCallback(adoptedCallback); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| |
| auto* attributeChangedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(vm, "attributeChangedCallback")); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| if (attributeChangedCallback) { |
| auto observedAttributesValue = constructor->get(&state, Identifier::fromString(vm, "observedAttributes")); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| if (!observedAttributesValue.isUndefined()) { |
| auto observedAttributes = convert<IDLSequence<IDLDOMString>>(state, observedAttributesValue); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| elementInterface->setAttributeChangedCallback(attributeChangedCallback, observedAttributes); |
| } |
| } |
| |
| auto addToGlobalObjectWithPrivateName = [&] (JSObject* objectToAdd) { |
| if (objectToAdd) { |
| PrivateName uniquePrivateName; |
| globalObject()->putDirect(vm, uniquePrivateName, objectToAdd); |
| } |
| }; |
| |
| addToGlobalObjectWithPrivateName(constructor); |
| addToGlobalObjectWithPrivateName(connectedCallback); |
| addToGlobalObjectWithPrivateName(disconnectedCallback); |
| addToGlobalObjectWithPrivateName(adoptedCallback); |
| addToGlobalObjectWithPrivateName(attributeChangedCallback); |
| |
| registry.addElementDefinition(WTFMove(elementInterface)); |
| |
| return jsUndefined(); |
| } |
| |
| // https://html.spec.whatwg.org/#dom-customelementregistry-whendefined |
| static JSValue whenDefinedPromise(ExecState& state, JSDOMGlobalObject& globalObject, CustomElementRegistry& registry, JSPromiseDeferred& promiseDeferred) |
| { |
| auto scope = DECLARE_THROW_SCOPE(state.vm()); |
| |
| if (UNLIKELY(state.argumentCount() < 1)) |
| return throwException(&state, scope, createNotEnoughArgumentsError(&state)); |
| |
| AtomString localName(state.uncheckedArgument(0).toString(&state)->toAtomString(&state)); |
| RETURN_IF_EXCEPTION(scope, JSValue()); |
| |
| if (!validateCustomElementNameAndThrowIfNeeded(state, localName)) { |
| EXCEPTION_ASSERT(scope.exception()); |
| return jsUndefined(); |
| } |
| |
| if (registry.findInterface(localName)) { |
| DeferredPromise::create(globalObject, promiseDeferred)->resolve(); |
| return promiseDeferred.promise(); |
| } |
| |
| auto result = registry.promiseMap().ensure(localName, [&] { |
| return DeferredPromise::create(globalObject, promiseDeferred); |
| }); |
| |
| return result.iterator->value->promise(); |
| } |
| |
| JSValue JSCustomElementRegistry::whenDefined(ExecState& state) |
| { |
| auto scope = DECLARE_CATCH_SCOPE(state.vm()); |
| |
| ASSERT(globalObject()); |
| auto promiseDeferred = JSPromiseDeferred::tryCreate(&state, globalObject()); |
| RELEASE_ASSERT(promiseDeferred); |
| JSValue promise = whenDefinedPromise(state, *globalObject(), wrapped(), *promiseDeferred); |
| |
| if (UNLIKELY(scope.exception())) { |
| rejectPromiseWithExceptionIfAny(state, *globalObject(), *promiseDeferred); |
| scope.assertNoException(); |
| return promiseDeferred->promise(); |
| } |
| |
| return promise; |
| } |
| |
| } |