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, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
6 * Copyright (C) 2009 Rob Buis (rwlbuis@gmail.com)
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 "HTMLLinkElement.h"
26
27#include "CSSHelper.h"
28#include "CachedCSSStyleSheet.h"
29#include "DNS.h"
30#include "DocLoader.h"
31#include "Document.h"
32#include "Frame.h"
33#include "FrameLoader.h"
34#include "FrameLoaderClient.h"
35#include "FrameTree.h"
36#include "HTMLNames.h"
37#include "MappedAttribute.h"
38#include "MediaList.h"
39#include "MediaQueryEvaluator.h"
40#include "Page.h"
41#include "ScriptEventListener.h"
42#include "Settings.h"
43#include <wtf/StdLibExtras.h>
44
45namespace WebCore {
46
47using namespace HTMLNames;
48
49HTMLLinkElement::HTMLLinkElement(const QualifiedName& qName, Document *doc, bool createdByParser)
50    : HTMLElement(qName, doc)
51    , m_cachedSheet(0)
52    , m_disabledState(0)
53    , m_loading(false)
54    , m_alternate(false)
55    , m_isStyleSheet(false)
56    , m_isIcon(false)
57#ifdef ANDROID_APPLE_TOUCH_ICON
58    , m_isTouchIcon(false)
59    , m_isPrecomposedTouchIcon(false)
60#endif
61    , m_isDNSPrefetch(false)
62    , m_createdByParser(createdByParser)
63{
64    ASSERT(hasTagName(linkTag));
65}
66
67HTMLLinkElement::~HTMLLinkElement()
68{
69    if (m_cachedSheet) {
70        m_cachedSheet->removeClient(this);
71        if (m_loading && !isDisabled() && !isAlternate())
72            document()->removePendingSheet();
73    }
74}
75
76void HTMLLinkElement::setDisabledState(bool _disabled)
77{
78    int oldDisabledState = m_disabledState;
79    m_disabledState = _disabled ? 2 : 1;
80    if (oldDisabledState != m_disabledState) {
81        // If we change the disabled state while the sheet is still loading, then we have to
82        // perform three checks:
83        if (isLoading()) {
84            // Check #1: If the sheet becomes disabled while it was loading, and if it was either
85            // a main sheet or a sheet that was previously enabled via script, then we need
86            // to remove it from the list of pending sheets.
87            if (m_disabledState == 2 && (!m_alternate || oldDisabledState == 1))
88                document()->removePendingSheet();
89
90            // Check #2: An alternate sheet becomes enabled while it is still loading.
91            if (m_alternate && m_disabledState == 1)
92                document()->addPendingSheet();
93
94            // Check #3: A main sheet becomes enabled while it was still loading and
95            // after it was disabled via script.  It takes really terrible code to make this
96            // happen (a double toggle for no reason essentially).  This happens on
97            // virtualplastic.net, which manages to do about 12 enable/disables on only 3
98            // sheets. :)
99            if (!m_alternate && m_disabledState == 1 && oldDisabledState == 2)
100                document()->addPendingSheet();
101
102            // If the sheet is already loading just bail.
103            return;
104        }
105
106        // Load the sheet, since it's never been loaded before.
107        if (!m_sheet && m_disabledState == 1)
108            process();
109        else
110            document()->updateStyleSelector(); // Update the style selector.
111    }
112}
113
114StyleSheet* HTMLLinkElement::sheet() const
115{
116    return m_sheet.get();
117}
118
119void HTMLLinkElement::parseMappedAttribute(MappedAttribute *attr)
120{
121    if (attr->name() == relAttr) {
122#ifdef ANDROID_APPLE_TOUCH_ICON
123        tokenizeRelAttribute(attr->value(), m_isStyleSheet, m_alternate, m_isIcon, m_isTouchIcon, m_isPrecomposedTouchIcon, m_isDNSPrefetch);
124#else
125        tokenizeRelAttribute(attr->value(), m_isStyleSheet, m_alternate, m_isIcon, m_isDNSPrefetch);
126#endif
127        process();
128    } else if (attr->name() == hrefAttr) {
129        m_url = document()->completeURL(deprecatedParseURL(attr->value()));
130        process();
131    } else if (attr->name() == typeAttr) {
132        m_type = attr->value();
133        process();
134    } else if (attr->name() == mediaAttr) {
135        m_media = attr->value().string().lower();
136        process();
137    } else if (attr->name() == disabledAttr) {
138        setDisabledState(!attr->isNull());
139    } else if (attr->name() == onbeforeloadAttr)
140        setAttributeEventListener(eventNames().beforeloadEvent, createAttributeEventListener(this, attr));
141    else {
142        if (attr->name() == titleAttr && m_sheet)
143            m_sheet->setTitle(attr->value());
144        HTMLElement::parseMappedAttribute(attr);
145    }
146}
147
148#ifdef ANDROID_APPLE_TOUCH_ICON
149void HTMLLinkElement::tokenizeRelAttribute(const AtomicString& rel, bool& styleSheet, bool& alternate, bool& icon, bool& touchIcon, bool& precomposedTouchIcon, bool& dnsPrefetch)
150#else
151void HTMLLinkElement::tokenizeRelAttribute(const AtomicString& rel, bool& styleSheet, bool& alternate, bool& icon, bool& dnsPrefetch)
152#endif
153{
154    styleSheet = false;
155    icon = false;
156    alternate = false;
157    dnsPrefetch = false;
158#ifdef ANDROID_APPLE_TOUCH_ICON
159    touchIcon = false;
160    precomposedTouchIcon = false;
161#endif
162    if (equalIgnoringCase(rel, "stylesheet"))
163        styleSheet = true;
164    else if (equalIgnoringCase(rel, "icon") || equalIgnoringCase(rel, "shortcut icon"))
165        icon = true;
166#ifdef ANDROID_APPLE_TOUCH_ICON
167    else if (equalIgnoringCase(rel, "apple-touch-icon"))
168        touchIcon = true;
169    else if (equalIgnoringCase(rel, "apple-touch-icon-precomposed"))
170        precomposedTouchIcon = true;
171#endif
172    else if (equalIgnoringCase(rel, "dns-prefetch"))
173        dnsPrefetch = true;
174    else if (equalIgnoringCase(rel, "alternate stylesheet") || equalIgnoringCase(rel, "stylesheet alternate")) {
175        styleSheet = true;
176        alternate = true;
177    } else {
178        // Tokenize the rel attribute and set bits based on specific keywords that we find.
179        String relString = rel.string();
180        relString.replace('\n', ' ');
181        Vector<String> list;
182        relString.split(' ', list);
183        Vector<String>::const_iterator end = list.end();
184        for (Vector<String>::const_iterator it = list.begin(); it != end; ++it) {
185            if (equalIgnoringCase(*it, "stylesheet"))
186                styleSheet = true;
187            else if (equalIgnoringCase(*it, "alternate"))
188                alternate = true;
189            else if (equalIgnoringCase(*it, "icon"))
190                icon = true;
191        }
192    }
193}
194
195void HTMLLinkElement::process()
196{
197    if (!inDocument())
198        return;
199
200    String type = m_type.lower();
201
202    // IE extension: location of small icon for locationbar / bookmarks
203    // We'll record this URL per document, even if we later only use it in top level frames
204    if (m_isIcon && m_url.isValid() && !m_url.isEmpty())
205        document()->setIconURL(m_url.string(), type);
206
207#ifdef ANDROID_APPLE_TOUCH_ICON
208    if ((m_isTouchIcon || m_isPrecomposedTouchIcon) && m_url.isValid()
209            && !m_url.isEmpty())
210        document()->frame()->loader()->client()
211                ->dispatchDidReceiveTouchIconURL(m_url.string(),
212                        m_isPrecomposedTouchIcon);
213#endif
214
215    if (m_isDNSPrefetch && m_url.isValid() && !m_url.isEmpty())
216        prefetchDNS(m_url.host());
217
218    bool acceptIfTypeContainsTextCSS = document()->page() && document()->page()->settings() && document()->page()->settings()->treatsAnyTextCSSLinkAsStylesheet();
219
220    // Stylesheet
221    // This was buggy and would incorrectly match <link rel="alternate">, which has a different specified meaning. -dwh
222    if (m_disabledState != 2 && (m_isStyleSheet || (acceptIfTypeContainsTextCSS && type.contains("text/css"))) && document()->frame() && m_url.isValid()) {
223        // also, don't load style sheets for standalone documents
224
225        String charset = getAttribute(charsetAttr);
226        if (charset.isEmpty() && document()->frame())
227            charset = document()->frame()->loader()->encoding();
228
229        if (m_cachedSheet) {
230            if (m_loading)
231                document()->removePendingSheet();
232            m_cachedSheet->removeClient(this);
233            m_cachedSheet = 0;
234        }
235
236        if (!dispatchBeforeLoadEvent(m_url))
237            return;
238
239        m_loading = true;
240
241        // Add ourselves as a pending sheet, but only if we aren't an alternate
242        // stylesheet.  Alternate stylesheets don't hold up render tree construction.
243        if (!isAlternate())
244            document()->addPendingSheet();
245
246        m_cachedSheet = document()->docLoader()->requestCSSStyleSheet(m_url, charset);
247
248        if (m_cachedSheet)
249            m_cachedSheet->addClient(this);
250        else {
251            // The request may have been denied if (for example) the stylesheet is local and the document is remote.
252            m_loading = false;
253            if (!isAlternate())
254                document()->removePendingSheet();
255        }
256    } else if (m_sheet) {
257        // we no longer contain a stylesheet, e.g. perhaps rel or type was changed
258        m_sheet = 0;
259        document()->updateStyleSelector();
260    }
261}
262
263void HTMLLinkElement::insertedIntoDocument()
264{
265    HTMLElement::insertedIntoDocument();
266    document()->addStyleSheetCandidateNode(this, m_createdByParser);
267    process();
268}
269
270void HTMLLinkElement::removedFromDocument()
271{
272    HTMLElement::removedFromDocument();
273
274    document()->removeStyleSheetCandidateNode(this);
275
276    // FIXME: It's terrible to do a synchronous update of the style selector just because a <style> or <link> element got removed.
277    if (document()->renderer())
278        document()->updateStyleSelector();
279}
280
281void HTMLLinkElement::finishParsingChildren()
282{
283    m_createdByParser = false;
284    HTMLElement::finishParsingChildren();
285}
286
287void HTMLLinkElement::setCSSStyleSheet(const String& href, const KURL& baseURL, const String& charset, const CachedCSSStyleSheet* sheet)
288{
289    m_sheet = CSSStyleSheet::create(this, href, baseURL, charset);
290
291    bool strictParsing = !document()->inCompatMode();
292    bool enforceMIMEType = strictParsing;
293    bool crossOriginCSS = false;
294    bool validMIMEType = false;
295    bool needsSiteSpecificQuirks = document()->page() && document()->page()->settings()->needsSiteSpecificQuirks();
296
297    // Check to see if we should enforce the MIME type of the CSS resource in strict mode.
298    // Running in iWeb 2 is one example of where we don't want to - <rdar://problem/6099748>
299    if (enforceMIMEType && document()->page() && !document()->page()->settings()->enforceCSSMIMETypeInStrictMode())
300        enforceMIMEType = false;
301
302#if defined(BUILDING_ON_TIGER) || defined(BUILDING_ON_LEOPARD)
303    if (enforceMIMEType && needsSiteSpecificQuirks) {
304        // Covers both http and https, with or without "www."
305        if (baseURL.string().contains("mcafee.com/japan/", false))
306            enforceMIMEType = false;
307    }
308#endif
309
310    String sheetText = sheet->sheetText(enforceMIMEType, &validMIMEType);
311    m_sheet->parseString(sheetText, strictParsing);
312
313    // If we're loading a stylesheet cross-origin, and the MIME type is not
314    // standard, require the CSS to at least start with a syntactically
315    // valid CSS rule.
316    // This prevents an attacker playing games by injecting CSS strings into
317    // HTML, XML, JSON, etc. etc.
318    if (!document()->securityOrigin()->canRequest(baseURL))
319        crossOriginCSS = true;
320
321    if (crossOriginCSS && !validMIMEType && !m_sheet->hasSyntacticallyValidCSSHeader())
322        m_sheet = CSSStyleSheet::create(this, href, baseURL, charset);
323
324    if (strictParsing && needsSiteSpecificQuirks) {
325        // Work around <https://bugs.webkit.org/show_bug.cgi?id=28350>.
326        DEFINE_STATIC_LOCAL(const String, slashKHTMLFixesDotCss, ("/KHTMLFixes.css"));
327        DEFINE_STATIC_LOCAL(const String, mediaWikiKHTMLFixesStyleSheet, ("/* KHTML fix stylesheet */\n/* work around the horizontal scrollbars */\n#column-content { margin-left: 0; }\n\n"));
328        // There are two variants of KHTMLFixes.css. One is equal to mediaWikiKHTMLFixesStyleSheet,
329        // while the other lacks the second trailing newline.
330        if (baseURL.string().endsWith(slashKHTMLFixesDotCss) && !sheetText.isNull() && mediaWikiKHTMLFixesStyleSheet.startsWith(sheetText)
331                && sheetText.length() >= mediaWikiKHTMLFixesStyleSheet.length() - 1) {
332            ASSERT(m_sheet->length() == 1);
333            ExceptionCode ec;
334            m_sheet->deleteRule(0, ec);
335        }
336    }
337
338    m_sheet->setTitle(title());
339
340    RefPtr<MediaList> media = MediaList::createAllowingDescriptionSyntax(m_media);
341    m_sheet->setMedia(media.get());
342
343    m_loading = false;
344    m_sheet->checkLoaded();
345}
346
347bool HTMLLinkElement::isLoading() const
348{
349    if (m_loading)
350        return true;
351    if (!m_sheet)
352        return false;
353    return static_cast<CSSStyleSheet *>(m_sheet.get())->isLoading();
354}
355
356bool HTMLLinkElement::sheetLoaded()
357{
358    if (!isLoading() && !isDisabled() && !isAlternate()) {
359        document()->removePendingSheet();
360        return true;
361    }
362    return false;
363}
364
365bool HTMLLinkElement::isURLAttribute(Attribute *attr) const
366{
367    return attr->name() == hrefAttr;
368}
369
370bool HTMLLinkElement::disabled() const
371{
372    return !getAttribute(disabledAttr).isNull();
373}
374
375void HTMLLinkElement::setDisabled(bool disabled)
376{
377    setAttribute(disabledAttr, disabled ? "" : 0);
378}
379
380String HTMLLinkElement::charset() const
381{
382    return getAttribute(charsetAttr);
383}
384
385void HTMLLinkElement::setCharset(const String& value)
386{
387    setAttribute(charsetAttr, value);
388}
389
390KURL HTMLLinkElement::href() const
391{
392    return document()->completeURL(getAttribute(hrefAttr));
393}
394
395void HTMLLinkElement::setHref(const String& value)
396{
397    setAttribute(hrefAttr, value);
398}
399
400String HTMLLinkElement::hreflang() const
401{
402    return getAttribute(hreflangAttr);
403}
404
405void HTMLLinkElement::setHreflang(const String& value)
406{
407    setAttribute(hreflangAttr, value);
408}
409
410String HTMLLinkElement::media() const
411{
412    return getAttribute(mediaAttr);
413}
414
415void HTMLLinkElement::setMedia(const String& value)
416{
417    setAttribute(mediaAttr, value);
418}
419
420String HTMLLinkElement::rel() const
421{
422    return getAttribute(relAttr);
423}
424
425void HTMLLinkElement::setRel(const String& value)
426{
427    setAttribute(relAttr, value);
428}
429
430String HTMLLinkElement::rev() const
431{
432    return getAttribute(revAttr);
433}
434
435void HTMLLinkElement::setRev(const String& value)
436{
437    setAttribute(revAttr, value);
438}
439
440String HTMLLinkElement::target() const
441{
442    return getAttribute(targetAttr);
443}
444
445void HTMLLinkElement::setTarget(const String& value)
446{
447    setAttribute(targetAttr, value);
448}
449
450String HTMLLinkElement::type() const
451{
452    return getAttribute(typeAttr);
453}
454
455void HTMLLinkElement::setType(const String& value)
456{
457    setAttribute(typeAttr, value);
458}
459
460void HTMLLinkElement::addSubresourceAttributeURLs(ListHashSet<KURL>& urls) const
461{
462    HTMLElement::addSubresourceAttributeURLs(urls);
463
464    // Favicons are handled by a special case in LegacyWebArchive::create()
465    if (m_isIcon)
466        return;
467
468    if (!m_isStyleSheet)
469        return;
470
471    // Append the URL of this link element.
472    addSubresourceURL(urls, href());
473
474    // Walk the URLs linked by the linked-to stylesheet.
475    if (StyleSheet* styleSheet = const_cast<HTMLLinkElement*>(this)->sheet())
476        styleSheet->addSubresourceStyleURLs(urls);
477}
478
479#ifdef ANDROID_INSTRUMENT
480void* HTMLLinkElement::operator new(size_t size)
481{
482    return Node::operator new(size);
483}
484
485void* HTMLLinkElement::operator new[](size_t size)
486{
487    return Node::operator new[](size);
488}
489
490void HTMLLinkElement::operator delete(void* p, size_t size)
491{
492    Node::operator delete(p, size);
493}
494
495void HTMLLinkElement::operator delete[](void* p, size_t size)
496{
497    Node::operator delete[](p, size);
498}
499#endif
500
501}
502