/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ package org.lobobrowser.html.style; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import org.eclipse.jdt.annotation.NonNull; import org.lobobrowser.html.domimpl.HTMLDocumentImpl; import org.lobobrowser.html.domimpl.HTMLElementImpl; import org.lobobrowser.ua.NetworkRequest; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.ua.UserAgentContext.Request; import org.lobobrowser.ua.UserAgentContext.RequestKind; import org.lobobrowser.util.SecurityUtil; import org.lobobrowser.util.Strings; import org.lobobrowser.util.Urls; import org.w3c.css.sac.InputSource; import org.w3c.dom.stylesheets.MediaList; import cz.vutbr.web.css.CSSException; import cz.vutbr.web.css.CSSFactory; import cz.vutbr.web.css.MediaSpec; import cz.vutbr.web.css.NetworkProcessor; import cz.vutbr.web.css.RuleFactory; import cz.vutbr.web.css.StyleSheet; import cz.vutbr.web.csskit.RuleFactoryImpl; import cz.vutbr.web.csskit.antlr4.CSSParserFactory; import cz.vutbr.web.csskit.antlr4.CSSParserFactory.SourceType; public class CSSUtilities { private static final Logger logger = Logger.getLogger(CSSUtilities.class.getName()); private static final RuleFactory rf = RuleFactoryImpl.getInstance(); private CSSUtilities() { } public static String preProcessCss(final String text) { try { final BufferedReader reader = new BufferedReader(new StringReader(text)); String line; final StringBuffer sb = new StringBuffer(); String pendingLine = null; // Only last line should be trimmed. while ((line = reader.readLine()) != null) { final String tline = line.trim(); if (tline.length() != 0) { if (pendingLine != null) { sb.append(pendingLine); sb.append("\r\n"); pendingLine = null; } if (tline.startsWith("//")) { pendingLine = line; continue; } sb.append(line); sb.append("\r\n"); } } return sb.toString(); } catch (final IOException ioe) { // not possible throw new IllegalStateException(ioe.getMessage()); } } public static InputSource getCssInputSourceForStyleSheet(final String text, final String scriptURI) { final java.io.Reader reader = new StringReader(text); final InputSource is = new InputSource(reader); is.setURI(scriptURI); return is; } public static StyleSheet jParseStyleSheet(final org.w3c.dom.Node ownerNode, final String baseURI, final String stylesheetStr, final UserAgentContext bcontext) { return jParseCSS2(ownerNode, baseURI, stylesheetStr, bcontext); } public static StyleSheet jParse(final org.w3c.dom.Node ownerNode, final String href, final HTMLDocumentImpl doc, final String baseUri, final boolean considerDoubleSlashComments) throws MalformedURLException { final UserAgentContext bcontext = doc.getUserAgentContext(); final NetworkRequest request = bcontext.createHttpRequest(); final URL baseURL = new URL(baseUri); final URL cssURL = Urls.createURL(baseURL, href); final String cssURI = cssURL.toExternalForm(); // Perform a synchronous request SecurityUtil.doPrivileged(() -> { try { request.open("GET", cssURI, false); request.send(null, new Request(cssURL, RequestKind.CSS)); } catch (final java.io.IOException thrown) { logger.log(Level.WARNING, "parse()", thrown); } return getEmptyStyleSheet(); }); final int status = request.getStatus(); if ((status != 200) && (status != 0)) { logger.warning("Unable to parse CSS. URI=[" + cssURI + "]. Response status was " + status + "."); return getEmptyStyleSheet(); } final String text = request.getResponseText(); if ((text != null) && !"".equals(text)) { final String processedText = considerDoubleSlashComments ? preProcessCss(text) : text; return jParseCSS2(ownerNode, cssURI, processedText, bcontext); } else { return getEmptyStyleSheet(); } } public static StyleSheet getEmptyStyleSheet() { final StyleSheet css = rf.createStyleSheet(); css.unlock(); return css; } private static StyleSheet jParseCSS2(final org.w3c.dom.Node ownerNode, final String cssURI, final String processedText, final UserAgentContext bcontext) { try { final URL base = new URL(cssURI); CSSFactory.setAutoImportMedia(new MediaSpec("screen")); return CSSParserFactory.getInstance().parse(processedText, new SafeNetworkProcessor(bcontext), "utf-8", SourceType.EMBEDDED, base); } catch (IOException | CSSException e) { logger.log(Level.SEVERE, "Unable to parse CSS. URI=[" + cssURI + "].", e); return getEmptyStyleSheet(); } } public static class SafeNetworkProcessor implements NetworkProcessor { final UserAgentContext bcontext; public SafeNetworkProcessor(final UserAgentContext bcontext) { this.bcontext = bcontext; } @Override public InputStream fetch(final @NonNull URL url) throws IOException { try { return AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> { final NetworkRequest request = bcontext.createHttpRequest(); request.open("GET", url, false); request.send(null, new Request(url, RequestKind.CSS)); final byte[] responseBytes = request.getResponseBytes(); if (responseBytes == null) { // This can happen when a request is denied by the request manager. throw new IOException("Empty response"); } else { return new ByteArrayInputStream(responseBytes); } }); } catch (final PrivilegedActionException e) { if (e.getException() instanceof IOException) { throw (IOException) e.getException(); } else { throw new RuntimeException(e); } } } } public static StyleSheet jParseInlineStyle(final String style, final String encoding, final HTMLElementImpl element, final boolean inlinePriority) { try { return CSSParserFactory.getInstance().parse(style, new SafeNetworkProcessor(null), null, SourceType.INLINE, element, inlinePriority, element.getDocumentURL()); } catch (IOException | CSSException e) { logger.log(Level.SEVERE, "Unable to parse CSS. CSS=[" + style + "].", e); return getEmptyStyleSheet(); } } public static boolean matchesMedia(final String mediaValues, final UserAgentContext rcontext) { if ((mediaValues == null) || (mediaValues.length() == 0)) { return true; } if (rcontext == null) { return false; } final StringTokenizer tok = new StringTokenizer(mediaValues, ","); while (tok.hasMoreTokens()) { final String token = tok.nextToken().trim(); final String mediaName = Strings.trimForAlphaNumDash(token); if (rcontext.isMedia(mediaName)) { return true; } } return false; } public static boolean matchesMedia(final MediaList mediaList, final UserAgentContext rcontext) { if (mediaList == null) { return true; } final int length = mediaList.getLength(); if (length == 0) { return true; } if (rcontext == null) { return false; } for (int i = 0; i < length; i++) { final String mediaName = mediaList.item(i); if (rcontext.isMedia(mediaName)) { return true; } } return false; } }