1/*
2 * Copyright (C) 2015-2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "JSCustomElementRegistry.h"
28
29#include "CustomElementRegistry.h"
30#include "Document.h"
31#include "HTMLNames.h"
32#include "JSCustomElementInterface.h"
33#include "JSDOMBinding.h"
34#include "JSDOMConvertSequences.h"
35#include "JSDOMConvertStrings.h"
36#include "JSDOMPromiseDeferred.h"
37#include <wtf/SetForScope.h>
38
39
40namespace WebCore {
41using namespace JSC;
42
43static JSObject* getCustomElementCallback(ExecState& state, JSObject& prototype, const Identifier& id)
44{
45 VM& vm = state.vm();
46 auto scope = DECLARE_THROW_SCOPE(vm);
47
48 JSValue callback = prototype.get(&state, id);
49 RETURN_IF_EXCEPTION(scope, nullptr);
50 if (callback.isUndefined())
51 return nullptr;
52 if (!callback.isFunction(vm)) {
53 throwTypeError(&state, scope, "A custom element callback must be a function"_s);
54 return nullptr;
55 }
56 return callback.getObject();
57}
58
59static bool validateCustomElementNameAndThrowIfNeeded(ExecState& state, const AtomicString& name)
60{
61 auto scope = DECLARE_THROW_SCOPE(state.vm());
62 switch (Document::validateCustomElementName(name)) {
63 case CustomElementNameValidationStatus::Valid:
64 return true;
65 case CustomElementNameValidationStatus::FirstCharacterIsNotLowercaseASCIILetter:
66 throwDOMSyntaxError(state, scope, "Custom element name must have a lowercase ASCII letter as its first character"_s);
67 return false;
68 case CustomElementNameValidationStatus::ContainsUppercaseASCIILetter:
69 throwDOMSyntaxError(state, scope, "Custom element name cannot contain an uppercase ASCII letter"_s);
70 return false;
71 case CustomElementNameValidationStatus::ContainsNoHyphen:
72 throwDOMSyntaxError(state, scope, "Custom element name must contain a hyphen"_s);
73 return false;
74 case CustomElementNameValidationStatus::ContainsDisallowedCharacter:
75 throwDOMSyntaxError(state, scope, "Custom element name contains a character that is not allowed"_s);
76 return false;
77 case CustomElementNameValidationStatus::ConflictsWithStandardElementName:
78 throwDOMSyntaxError(state, scope, "Custom element name cannot be same as one of the standard elements"_s);
79 return false;
80 }
81 ASSERT_NOT_REACHED();
82 return false;
83}
84
85// https://html.spec.whatwg.org/#dom-customelementregistry-define
86JSValue JSCustomElementRegistry::define(ExecState& state)
87{
88 VM& vm = state.vm();
89 auto scope = DECLARE_THROW_SCOPE(vm);
90
91 if (UNLIKELY(state.argumentCount() < 2))
92 return throwException(&state, scope, createNotEnoughArgumentsError(&state));
93
94 AtomicString localName(state.uncheckedArgument(0).toString(&state)->toAtomicString(&state));
95 RETURN_IF_EXCEPTION(scope, JSValue());
96
97 JSValue constructorValue = state.uncheckedArgument(1);
98 if (!constructorValue.isConstructor(vm))
99 return throwTypeError(&state, scope, "The second argument must be a constructor"_s);
100 JSObject* constructor = constructorValue.getObject();
101
102 if (!validateCustomElementNameAndThrowIfNeeded(state, localName))
103 return jsUndefined();
104
105 CustomElementRegistry& registry = wrapped();
106
107 if (registry.elementDefinitionIsRunning()) {
108 throwNotSupportedError(state, scope, "Cannot define a custom element while defining another custom element"_s);
109 return jsUndefined();
110 }
111 SetForScope<bool> change(registry.elementDefinitionIsRunning(), true);
112
113 if (registry.findInterface(localName)) {
114 throwNotSupportedError(state, scope, "Cannot define multiple custom elements with the same tag name"_s);
115 return jsUndefined();
116 }
117
118 if (registry.containsConstructor(constructor)) {
119 throwNotSupportedError(state, scope, "Cannot define multiple custom elements with the same class"_s);
120 return jsUndefined();
121 }
122
123 JSValue prototypeValue = constructor->get(&state, vm.propertyNames->prototype);
124 RETURN_IF_EXCEPTION(scope, JSValue());
125 if (!prototypeValue.isObject())
126 return throwTypeError(&state, scope, "Custom element constructor's prototype must be an object"_s);
127 JSObject& prototypeObject = *asObject(prototypeValue);
128
129 QualifiedName name(nullAtom(), localName, HTMLNames::xhtmlNamespaceURI);
130 auto elementInterface = JSCustomElementInterface::create(name, constructor, globalObject());
131
132 auto* connectedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(&vm, "connectedCallback"));
133 if (connectedCallback)
134 elementInterface->setConnectedCallback(connectedCallback);
135 RETURN_IF_EXCEPTION(scope, JSValue());
136
137 auto* disconnectedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(&vm, "disconnectedCallback"));
138 if (disconnectedCallback)
139 elementInterface->setDisconnectedCallback(disconnectedCallback);
140 RETURN_IF_EXCEPTION(scope, JSValue());
141
142 auto* adoptedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(&vm, "adoptedCallback"));
143 if (adoptedCallback)
144 elementInterface->setAdoptedCallback(adoptedCallback);
145 RETURN_IF_EXCEPTION(scope, JSValue());
146
147 auto* attributeChangedCallback = getCustomElementCallback(state, prototypeObject, Identifier::fromString(&vm, "attributeChangedCallback"));
148 RETURN_IF_EXCEPTION(scope, JSValue());
149 if (attributeChangedCallback) {
150 auto observedAttributesValue = constructor->get(&state, Identifier::fromString(&state, "observedAttributes"));
151 RETURN_IF_EXCEPTION(scope, JSValue());
152 if (!observedAttributesValue.isUndefined()) {
153 auto observedAttributes = convert<IDLSequence<IDLDOMString>>(state, observedAttributesValue);
154 RETURN_IF_EXCEPTION(scope, JSValue());
155 elementInterface->setAttributeChangedCallback(attributeChangedCallback, observedAttributes);
156 }
157 }
158
159 auto addToGlobalObjectWithPrivateName = [&] (JSObject* objectToAdd) {
160 if (objectToAdd) {
161 PrivateName uniquePrivateName;
162 globalObject()->putDirect(vm, uniquePrivateName, objectToAdd);
163 }
164 };
165
166 addToGlobalObjectWithPrivateName(constructor);
167 addToGlobalObjectWithPrivateName(connectedCallback);
168 addToGlobalObjectWithPrivateName(disconnectedCallback);
169 addToGlobalObjectWithPrivateName(adoptedCallback);
170 addToGlobalObjectWithPrivateName(attributeChangedCallback);
171
172 registry.addElementDefinition(WTFMove(elementInterface));
173
174 return jsUndefined();
175}
176
177// https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
178static JSValue whenDefinedPromise(ExecState& state, JSDOMGlobalObject& globalObject, CustomElementRegistry& registry, JSPromiseDeferred& promiseDeferred)
179{
180 auto scope = DECLARE_THROW_SCOPE(state.vm());
181
182 if (UNLIKELY(state.argumentCount() < 1))
183 return throwException(&state, scope, createNotEnoughArgumentsError(&state));
184
185 AtomicString localName(state.uncheckedArgument(0).toString(&state)->toAtomicString(&state));
186 RETURN_IF_EXCEPTION(scope, JSValue());
187
188 if (!validateCustomElementNameAndThrowIfNeeded(state, localName)) {
189 EXCEPTION_ASSERT(scope.exception());
190 return jsUndefined();
191 }
192
193 if (registry.findInterface(localName)) {
194 DeferredPromise::create(globalObject, promiseDeferred)->resolve();
195 return promiseDeferred.promise();
196 }
197
198 auto result = registry.promiseMap().ensure(localName, [&] {
199 return DeferredPromise::create(globalObject, promiseDeferred);
200 });
201
202 return result.iterator->value->promise();
203}
204
205JSValue JSCustomElementRegistry::whenDefined(ExecState& state)
206{
207 auto scope = DECLARE_CATCH_SCOPE(state.vm());
208
209 ASSERT(globalObject());
210 auto promiseDeferred = JSPromiseDeferred::tryCreate(&state, globalObject());
211 RELEASE_ASSERT(promiseDeferred);
212 JSValue promise = whenDefinedPromise(state, *globalObject(), wrapped(), *promiseDeferred);
213
214 if (UNLIKELY(scope.exception())) {
215 rejectPromiseWithExceptionIfAny(state, *globalObject(), *promiseDeferred);
216 scope.assertNoException();
217 return promiseDeferred->promise();
218 }
219
220 return promise;
221}
222
223}
224