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