1/*
2 * (C) 1999-2003 Lars Knoll (knoll@kde.org)
3 * Copyright (C) 2004, 2006, 2010, 2012 Apple Inc. All rights reserved.
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB.  If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20#include "config.h"
21#include "MediaList.h"
22
23#include "CSSImportRule.h"
24#include "CSSParser.h"
25#include "CSSStyleSheet.h"
26#include "Console.h"
27#include "DOMWindow.h"
28#include "Document.h"
29#include "ExceptionCode.h"
30#include "MediaFeatureNames.h"
31#include "MediaQuery.h"
32#include "MediaQueryExp.h"
33#include "ScriptableDocumentParser.h"
34#include <wtf/text/StringBuilder.h>
35
36namespace WebCore {
37
38/* MediaList is used to store 3 types of media related entities which mean the same:
39 * Media Queries, Media Types and Media Descriptors.
40 * Currently MediaList always tries to parse media queries and if parsing fails,
41 * tries to fallback to Media Descriptors if m_fallbackToDescriptor flag is set.
42 * Slight problem with syntax error handling:
43 * CSS 2.1 Spec (http://www.w3.org/TR/CSS21/media.html)
44 * specifies that failing media type parsing is a syntax error
45 * CSS 3 Media Queries Spec (http://www.w3.org/TR/css3-mediaqueries/)
46 * specifies that failing media query is a syntax error
47 * HTML 4.01 spec (http://www.w3.org/TR/REC-html40/present/styles.html#adef-media)
48 * specifies that Media Descriptors should be parsed with forward-compatible syntax
49 * DOM Level 2 Style Sheet spec (http://www.w3.org/TR/DOM-Level-2-Style/)
50 * talks about MediaList.mediaText and refers
51 *   -  to Media Descriptors of HTML 4.0 in context of StyleSheet
52 *   -  to Media Types of CSS 2.0 in context of CSSMediaRule and CSSImportRule
53 *
54 * These facts create situation where same (illegal) media specification may result in
55 * different parses depending on whether it is media attr of style element or part of
56 * css @media rule.
57 * <style media="screen and resolution > 40dpi"> ..</style> will be enabled on screen devices where as
58 * @media screen and resolution > 40dpi {..} will not.
59 * This gets more counter-intuitive in JavaScript:
60 * document.styleSheets[0].media.mediaText = "screen and resolution > 40dpi" will be ok and
61 * enabled, while
62 * document.styleSheets[0].cssRules[0].media.mediaText = "screen and resolution > 40dpi" will
63 * throw SYNTAX_ERR exception.
64 */
65
66MediaQuerySet::MediaQuerySet()
67    : m_fallbackToDescriptor(false)
68    , m_lastLine(0)
69{
70}
71
72MediaQuerySet::MediaQuerySet(const String& mediaString, bool fallbackToDescriptor)
73    : m_fallbackToDescriptor(fallbackToDescriptor)
74    , m_lastLine(0)
75{
76    bool success = parse(mediaString);
77    // FIXME: parsing can fail. The problem with failing constructor is that
78    // we would need additional flag saying MediaList is not valid
79    // Parse can fail only when fallbackToDescriptor == false, i.e when HTML4 media descriptor
80    // forward-compatible syntax is not in use.
81    // DOMImplementationCSS seems to mandate that media descriptors are used
82    // for both html and svg, even though svg:style doesn't use media descriptors
83    // Currently the only places where parsing can fail are
84    // creating <svg:style>, creating css media / import rules from js
85
86    // FIXME: This doesn't make much sense.
87    if (!success)
88        parse("invalid");
89}
90
91MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
92    : RefCounted<MediaQuerySet>()
93    , m_fallbackToDescriptor(o.m_fallbackToDescriptor)
94    , m_lastLine(o.m_lastLine)
95    , m_queries(o.m_queries.size())
96{
97    for (unsigned i = 0; i < m_queries.size(); ++i)
98        m_queries[i] = o.m_queries[i]->copy();
99}
100
101MediaQuerySet::~MediaQuerySet()
102{
103}
104
105static String parseMediaDescriptor(const String& string)
106{
107    // http://www.w3.org/TR/REC-html40/types.html#type-media-descriptors
108    // "Each entry is truncated just before the first character that isn't a
109    // US ASCII letter [a-zA-Z] (ISO 10646 hex 41-5a, 61-7a), digit [0-9] (hex 30-39),
110    // or hyphen (hex 2d)."
111    unsigned length = string.length();
112    unsigned i = 0;
113    for (; i < length; ++i) {
114        unsigned short c = string[i];
115        if (! ((c >= 'a' && c <= 'z')
116               || (c >= 'A' && c <= 'Z')
117               || (c >= '1' && c <= '9')
118               || (c == '-')))
119            break;
120    }
121    return string.left(i);
122}
123
124bool MediaQuerySet::parse(const String& mediaString)
125{
126    CSSParser parser(CSSStrictMode);
127
128    Vector<OwnPtr<MediaQuery> > result;
129    Vector<String> list;
130    mediaString.split(',', list);
131    for (unsigned i = 0; i < list.size(); ++i) {
132        String medium = list[i].stripWhiteSpace();
133        if (medium.isEmpty()) {
134            if (!m_fallbackToDescriptor)
135                return false;
136            continue;
137        }
138        OwnPtr<MediaQuery> mediaQuery = parser.parseMediaQuery(medium);
139        if (!mediaQuery) {
140            if (!m_fallbackToDescriptor)
141                return false;
142            String mediaDescriptor = parseMediaDescriptor(medium);
143            if (mediaDescriptor.isNull())
144                continue;
145            mediaQuery = adoptPtr(new MediaQuery(MediaQuery::None, mediaDescriptor, nullptr));
146        }
147        result.append(mediaQuery.release());
148    }
149    // ",,,," falls straight through, but is not valid unless fallback
150    if (!m_fallbackToDescriptor && list.isEmpty()) {
151        String strippedMediaString = mediaString.stripWhiteSpace();
152        if (!strippedMediaString.isEmpty())
153            return false;
154    }
155    m_queries.swap(result);
156    return true;
157}
158
159bool MediaQuerySet::add(const String& queryString)
160{
161    CSSParser parser(CSSStrictMode);
162
163    OwnPtr<MediaQuery> parsedQuery = parser.parseMediaQuery(queryString);
164    if (!parsedQuery && m_fallbackToDescriptor) {
165        String medium = parseMediaDescriptor(queryString);
166        if (!medium.isNull())
167            parsedQuery = adoptPtr(new MediaQuery(MediaQuery::None, medium, nullptr));
168    }
169    if (!parsedQuery)
170        return false;
171
172    m_queries.append(parsedQuery.release());
173    return true;
174}
175
176bool MediaQuerySet::remove(const String& queryStringToRemove)
177{
178    CSSParser parser(CSSStrictMode);
179
180    OwnPtr<MediaQuery> parsedQuery = parser.parseMediaQuery(queryStringToRemove);
181    if (!parsedQuery && m_fallbackToDescriptor) {
182        String medium = parseMediaDescriptor(queryStringToRemove);
183        if (!medium.isNull())
184            parsedQuery = adoptPtr(new MediaQuery(MediaQuery::None, medium, nullptr));
185    }
186    if (!parsedQuery)
187        return false;
188
189    for (size_t i = 0; i < m_queries.size(); ++i) {
190        MediaQuery* query = m_queries[i].get();
191        if (*query == *parsedQuery) {
192            m_queries.remove(i);
193            return true;
194        }
195    }
196    return false;
197}
198
199void MediaQuerySet::addMediaQuery(PassOwnPtr<MediaQuery> mediaQuery)
200{
201    m_queries.append(mediaQuery);
202}
203
204String MediaQuerySet::mediaText() const
205{
206    StringBuilder text;
207
208    bool first = true;
209    for (size_t i = 0; i < m_queries.size(); ++i) {
210        if (!first)
211            text.appendLiteral(", ");
212        else
213            first = false;
214        text.append(m_queries[i]->cssText());
215    }
216    return text.toString();
217}
218
219MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet)
220    : m_mediaQueries(mediaQueries)
221    , m_parentStyleSheet(parentSheet)
222    , m_parentRule(0)
223{
224}
225
226MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule)
227    : m_mediaQueries(mediaQueries)
228    , m_parentStyleSheet(0)
229    , m_parentRule(parentRule)
230{
231}
232
233MediaList::~MediaList()
234{
235}
236
237void MediaList::setMediaText(const String& value, ExceptionCode& ec)
238{
239    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
240
241    bool success = m_mediaQueries->parse(value);
242    if (!success) {
243        ec = SYNTAX_ERR;
244        return;
245    }
246    if (m_parentStyleSheet)
247        m_parentStyleSheet->didMutate();
248}
249
250String MediaList::item(unsigned index) const
251{
252    const Vector<OwnPtr<MediaQuery> >& queries = m_mediaQueries->queryVector();
253    if (index < queries.size())
254        return queries[index]->cssText();
255    return String();
256}
257
258void MediaList::deleteMedium(const String& medium, ExceptionCode& ec)
259{
260    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
261
262    bool success = m_mediaQueries->remove(medium);
263    if (!success) {
264        ec = NOT_FOUND_ERR;
265        return;
266    }
267    if (m_parentStyleSheet)
268        m_parentStyleSheet->didMutate();
269}
270
271void MediaList::appendMedium(const String& medium, ExceptionCode& ec)
272{
273    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
274
275    bool success = m_mediaQueries->add(medium);
276    if (!success) {
277        // FIXME: Should this really be INVALID_CHARACTER_ERR?
278        ec = INVALID_CHARACTER_ERR;
279        return;
280    }
281    if (m_parentStyleSheet)
282        m_parentStyleSheet->didMutate();
283}
284
285void MediaList::reattach(MediaQuerySet* mediaQueries)
286{
287    ASSERT(mediaQueries);
288    m_mediaQueries = mediaQueries;
289}
290
291#if ENABLE(RESOLUTION_MEDIA_QUERY)
292static void addResolutionWarningMessageToConsole(Document* document, const String& serializedExpression, const CSSPrimitiveValue* value)
293{
294    ASSERT(document);
295    ASSERT(value);
296
297    DEFINE_STATIC_LOCAL(String, mediaQueryMessage, (ASCIILiteral("Consider using 'dppx' units instead of '%replacementUnits%', as in CSS '%replacementUnits%' means dots-per-CSS-%lengthUnit%, not dots-per-physical-%lengthUnit%, so does not correspond to the actual '%replacementUnits%' of a screen. In media query expression: ")));
298    DEFINE_STATIC_LOCAL(String, mediaValueDPI, (ASCIILiteral("dpi")));
299    DEFINE_STATIC_LOCAL(String, mediaValueDPCM, (ASCIILiteral("dpcm")));
300    DEFINE_STATIC_LOCAL(String, lengthUnitInch, (ASCIILiteral("inch")));
301    DEFINE_STATIC_LOCAL(String, lengthUnitCentimeter, (ASCIILiteral("centimeter")));
302
303    String message;
304    if (value->isDotsPerInch())
305        message = String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch);
306    else if (value->isDotsPerCentimeter())
307        message = String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter);
308    else
309        ASSERT_NOT_REACHED();
310
311    message.append(serializedExpression);
312
313    document->addConsoleMessage(CSSMessageSource, DebugMessageLevel, message);
314}
315
316void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet)
317{
318    if (!mediaQuerySet || !document)
319        return;
320
321    const Vector<OwnPtr<MediaQuery> >& mediaQueries = mediaQuerySet->queryVector();
322    const size_t queryCount = mediaQueries.size();
323
324    if (!queryCount)
325        return;
326
327    for (size_t i = 0; i < queryCount; ++i) {
328        const MediaQuery* query = mediaQueries[i].get();
329        String mediaType = query->mediaType();
330        if (!query->ignored() && !equalIgnoringCase(mediaType, "print")) {
331            const Vector<OwnPtr<MediaQueryExp> >* exps = query->expressions();
332            for (size_t j = 0; j < exps->size(); ++j) {
333                const MediaQueryExp* exp = exps->at(j).get();
334                if (exp->mediaFeature() == MediaFeatureNames::resolutionMediaFeature || exp->mediaFeature() == MediaFeatureNames::max_resolutionMediaFeature || exp->mediaFeature() == MediaFeatureNames::min_resolutionMediaFeature) {
335                    CSSValue* cssValue =  exp->value();
336                    if (cssValue && cssValue->isPrimitiveValue()) {
337                        CSSPrimitiveValue* primitiveValue = static_cast<CSSPrimitiveValue*>(cssValue);
338                        if (primitiveValue->isDotsPerInch() || primitiveValue->isDotsPerCentimeter())
339                            addResolutionWarningMessageToConsole(document, mediaQuerySet->mediaText(), primitiveValue);
340                    }
341                }
342            }
343        }
344    }
345}
346#endif
347
348}
349