1/*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4 *           (C) 2001 Dirk Mueller (mueller@kde.org)
5 * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
6 * Copyright (C) 2008 Nikolas Zimmermann <zimmermann@kde.org>
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB.  If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24#include "config.h"
25#include "ScriptElement.h"
26
27#include "CachedScript.h"
28#include "CachedResourceLoader.h"
29#include "ContentSecurityPolicy.h"
30#include "Document.h"
31#include "DocumentParser.h"
32#include "Frame.h"
33#include "FrameLoader.h"
34#include "HTMLNames.h"
35#include "HTMLScriptElement.h"
36#include "IgnoreDestructiveWriteCountIncrementer.h"
37#include "MIMETypeRegistry.h"
38#include "Page.h"
39#include "ScriptController.h"
40#include "ScriptRunner.h"
41#include "ScriptSourceCode.h"
42#include "ScriptValue.h"
43#include "Settings.h"
44#include "Text.h"
45#include <wtf/StdLibExtras.h>
46#include <wtf/text/StringHash.h>
47
48#if ENABLE(SVG)
49#include "SVGNames.h"
50#include "SVGScriptElement.h"
51#endif
52
53namespace WebCore {
54
55ScriptElement::ScriptElement(Element* element, bool parserInserted, bool alreadyStarted)
56    : m_element(element)
57    , m_cachedScript(0)
58    , m_parserInserted(parserInserted)
59    , m_isExternalScript(false)
60    , m_alreadyStarted(alreadyStarted)
61    , m_haveFiredLoad(false)
62    , m_willBeParserExecuted(false)
63    , m_readyToBeParserExecuted(false)
64    , m_willExecuteWhenDocumentFinishedParsing(false)
65    , m_forceAsync(!parserInserted)
66    , m_willExecuteInOrder(false)
67{
68    ASSERT(m_element);
69}
70
71ScriptElement::~ScriptElement()
72{
73    stopLoadRequest();
74}
75
76void ScriptElement::insertedIntoDocument()
77{
78    if (!m_parserInserted)
79        prepareScript(); // FIXME: Provide a real starting line number here.
80}
81
82void ScriptElement::removedFromDocument()
83{
84    // Eventually stop loading any not-yet-finished content
85    stopLoadRequest();
86}
87
88void ScriptElement::childrenChanged()
89{
90    if (!m_parserInserted && m_element->inDocument())
91        prepareScript(); // FIXME: Provide a real starting line number here.
92}
93
94void ScriptElement::handleSourceAttribute(const String& sourceUrl)
95{
96    if (ignoresLoadRequest() || sourceUrl.isEmpty())
97        return;
98
99    prepareScript(); // FIXME: Provide a real starting line number here.
100}
101
102void ScriptElement::handleAsyncAttribute()
103{
104    m_forceAsync = false;
105}
106
107// Helper function
108static bool isLegacySupportedJavaScriptLanguage(const String& language)
109{
110    // Mozilla 1.8 accepts javascript1.0 - javascript1.7, but WinIE 7 accepts only javascript1.1 - javascript1.3.
111    // Mozilla 1.8 and WinIE 7 both accept javascript and livescript.
112    // WinIE 7 accepts ecmascript and jscript, but Mozilla 1.8 doesn't.
113    // Neither Mozilla 1.8 nor WinIE 7 accept leading or trailing whitespace.
114    // We want to accept all the values that either of these browsers accept, but not other values.
115
116    // FIXME: This function is not HTML5 compliant. These belong in the MIME registry as "text/javascript<version>" entries.
117    typedef HashSet<String, CaseFoldingHash> LanguageSet;
118    DEFINE_STATIC_LOCAL(LanguageSet, languages, ());
119    if (languages.isEmpty()) {
120        languages.add("javascript");
121        languages.add("javascript");
122        languages.add("javascript1.0");
123        languages.add("javascript1.1");
124        languages.add("javascript1.2");
125        languages.add("javascript1.3");
126        languages.add("javascript1.4");
127        languages.add("javascript1.5");
128        languages.add("javascript1.6");
129        languages.add("javascript1.7");
130        languages.add("livescript");
131        languages.add("ecmascript");
132        languages.add("jscript");
133    }
134
135    return languages.contains(language);
136}
137
138bool ScriptElement::isScriptTypeSupported(LegacyTypeSupport supportLegacyTypes) const
139{
140    // FIXME: isLegacySupportedJavaScriptLanguage() is not valid HTML5. It is used here to maintain backwards compatibility with existing layout tests. The specific violations are:
141    // - Allowing type=javascript. type= should only support MIME types, such as text/javascript.
142    // - Allowing a different set of languages for language= and type=. language= supports Javascript 1.1 and 1.4-1.6, but type= does not.
143
144    String type = typeAttributeValue();
145    String language = languageAttributeValue();
146    if (type.isEmpty() && language.isEmpty())
147        return true; // Assume text/javascript.
148    if (type.isEmpty()) {
149        type = "text/" + language.lower();
150        if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type) || isLegacySupportedJavaScriptLanguage(language))
151            return true;
152    } else if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type.stripWhiteSpace().lower()) || (supportLegacyTypes == AllowLegacyTypeInTypeAttribute && isLegacySupportedJavaScriptLanguage(type)))
153        return true;
154    return false;
155}
156
157// http://dev.w3.org/html5/spec/Overview.html#prepare-a-script
158bool ScriptElement::prepareScript(const TextPosition1& scriptStartPosition, LegacyTypeSupport supportLegacyTypes)
159{
160    if (m_alreadyStarted)
161        return false;
162
163    bool wasParserInserted;
164    if (m_parserInserted) {
165        wasParserInserted = true;
166        m_parserInserted = false;
167    } else
168        wasParserInserted = false;
169
170    if (wasParserInserted && !asyncAttributeValue())
171        m_forceAsync = true;
172
173    // FIXME: HTML5 spec says we should check that all children are either comments or empty text nodes.
174    if (!hasSourceAttribute() && !m_element->firstChild())
175        return false;
176
177    if (!m_element->inDocument())
178        return false;
179
180    if (!isScriptTypeSupported(supportLegacyTypes))
181        return false;
182
183    if (wasParserInserted) {
184        m_parserInserted = true;
185        m_forceAsync = false;
186    }
187
188    m_alreadyStarted = true;
189
190    // FIXME: If script is parser inserted, verify it's still in the original document.
191
192    // FIXME: Eventually we'd like to evaluate scripts which are inserted into a
193    // viewless document but this'll do for now.
194    // See http://bugs.webkit.org/show_bug.cgi?id=5727
195    if (!m_element->document()->frame())
196        return false;
197
198    if (!m_element->document()->frame()->script()->canExecuteScripts(AboutToExecuteScript))
199        return false;
200
201    // FIXME: This is non-standard. Remove this after https://bugs.webkit.org/show_bug.cgi?id=62412.
202    Node* ancestor = m_element->parentNode();
203    while (ancestor) {
204        if (ancestor->isSVGShadowRoot())
205            return false;
206        ancestor = ancestor->parentNode();
207    }
208
209    if (!isScriptForEventSupported())
210        return false;
211
212    if (!charsetAttributeValue().isEmpty())
213        m_characterEncoding = charsetAttributeValue();
214    else
215        m_characterEncoding = m_element->document()->charset();
216
217    if (hasSourceAttribute())
218        if (!requestScript(sourceAttributeValue()))
219            return false;
220
221    if (hasSourceAttribute() && deferAttributeValue() && m_parserInserted && !asyncAttributeValue()) {
222        m_willExecuteWhenDocumentFinishedParsing = true;
223        m_willBeParserExecuted = true;
224    } else if (hasSourceAttribute() && m_parserInserted && !asyncAttributeValue())
225        m_willBeParserExecuted = true;
226    else if (!hasSourceAttribute() && m_parserInserted && !m_element->document()->haveStylesheetsLoaded()) {
227        m_willBeParserExecuted = true;
228        m_readyToBeParserExecuted = true;
229    } else if (hasSourceAttribute() && !asyncAttributeValue() && !m_forceAsync) {
230        m_willExecuteInOrder = true;
231        m_element->document()->scriptRunner()->queueScriptForExecution(this, m_cachedScript, ScriptRunner::IN_ORDER_EXECUTION);
232        m_cachedScript->addClient(this);
233    } else if (hasSourceAttribute())
234        m_cachedScript->addClient(this);
235    else
236        executeScript(ScriptSourceCode(scriptContent(), m_element->document()->url(), scriptStartPosition));
237
238    return true;
239}
240
241bool ScriptElement::requestScript(const String& sourceUrl)
242{
243    RefPtr<Document> originalDocument = m_element->document();
244    if (!m_element->dispatchBeforeLoadEvent(sourceUrl))
245        return false;
246    if (!m_element->inDocument() || m_element->document() != originalDocument)
247        return false;
248
249    ASSERT(!m_cachedScript);
250    // FIXME: If sourceUrl is empty, we should dispatchErrorEvent().
251    m_cachedScript = m_element->document()->cachedResourceLoader()->requestScript(sourceUrl, scriptCharset());
252    m_isExternalScript = true;
253
254    if (m_cachedScript)
255        return true;
256
257    dispatchErrorEvent();
258    return false;
259}
260
261void ScriptElement::executeScript(const ScriptSourceCode& sourceCode)
262{
263    ASSERT(m_alreadyStarted);
264
265    if (sourceCode.isEmpty())
266        return;
267
268    if (!m_isExternalScript && !m_element->document()->contentSecurityPolicy()->allowInlineScript())
269        return;
270
271    RefPtr<Document> document = m_element->document();
272    ASSERT(document);
273    if (Frame* frame = document->frame()) {
274        {
275            IgnoreDestructiveWriteCountIncrementer ignoreDesctructiveWriteCountIncrementer(m_isExternalScript ? document.get() : 0);
276            // Create a script from the script element node, using the script
277            // block's source and the script block's type.
278            // Note: This is where the script is compiled and actually executed.
279            frame->script()->evaluate(sourceCode);
280        }
281
282        Document::updateStyleForAllDocuments();
283    }
284}
285
286void ScriptElement::stopLoadRequest()
287{
288    if (m_cachedScript) {
289        if (!m_willBeParserExecuted)
290            m_cachedScript->removeClient(this);
291        m_cachedScript = 0;
292    }
293}
294
295void ScriptElement::execute(CachedScript* cachedScript)
296{
297    ASSERT(!m_willBeParserExecuted);
298    ASSERT(cachedScript);
299    if (cachedScript->errorOccurred())
300        dispatchErrorEvent();
301    else {
302        executeScript(ScriptSourceCode(cachedScript));
303        dispatchLoadEvent();
304    }
305    cachedScript->removeClient(this);
306}
307
308void ScriptElement::notifyFinished(CachedResource* o)
309{
310    ASSERT(!m_willBeParserExecuted);
311    ASSERT_UNUSED(o, o == m_cachedScript);
312    if (m_willExecuteInOrder)
313        m_element->document()->scriptRunner()->notifyInOrderScriptReady();
314    else
315        m_element->document()->scriptRunner()->queueScriptForExecution(this, m_cachedScript, ScriptRunner::ASYNC_EXECUTION);
316    m_cachedScript = 0;
317}
318
319bool ScriptElement::ignoresLoadRequest() const
320{
321    return m_alreadyStarted || m_isExternalScript || m_parserInserted || !m_element->inDocument();
322}
323
324bool ScriptElement::isScriptForEventSupported() const
325{
326    String eventAttribute = eventAttributeValue();
327    String forAttribute = forAttributeValue();
328    if (!eventAttribute.isEmpty() && !forAttribute.isEmpty()) {
329        forAttribute = forAttribute.stripWhiteSpace();
330        if (!equalIgnoringCase(forAttribute, "window"))
331            return false;
332
333        eventAttribute = eventAttribute.stripWhiteSpace();
334        if (!equalIgnoringCase(eventAttribute, "onload") && !equalIgnoringCase(eventAttribute, "onload()"))
335            return false;
336    }
337    return true;
338}
339
340String ScriptElement::scriptContent() const
341{
342    Vector<UChar> val;
343    Text* firstTextNode = 0;
344    bool foundMultipleTextNodes = false;
345
346    for (Node* n = m_element->firstChild(); n; n = n->nextSibling()) {
347        if (!n->isTextNode())
348            continue;
349
350        Text* t = static_cast<Text*>(n);
351        if (foundMultipleTextNodes)
352            append(val, t->data());
353        else if (firstTextNode) {
354            append(val, firstTextNode->data());
355            append(val, t->data());
356            foundMultipleTextNodes = true;
357        } else
358            firstTextNode = t;
359    }
360
361    if (firstTextNode && !foundMultipleTextNodes)
362        return firstTextNode->data();
363
364    return String::adopt(val);
365}
366
367ScriptElement* toScriptElement(Element* element)
368{
369    if (element->isHTMLElement() && element->hasTagName(HTMLNames::scriptTag))
370        return static_cast<HTMLScriptElement*>(element);
371
372#if ENABLE(SVG)
373    if (element->isSVGElement() && element->hasTagName(SVGNames::scriptTag))
374        return static_cast<SVGScriptElement*>(element);
375#endif
376
377    return 0;
378}
379
380}
381