//
// $Id: CssValidator.java,v 1.42 2009-02-26 10:41:25 ylafon Exp $
// From Philippe Le Hegaret (Philippe.Le_Hegaret@sophia.inria.fr)
//
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html
package org.w3c.css.servlet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ProtocolException;
import java.net.URL;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.w3c.css.css.CssParser;
import org.w3c.css.css.DocumentParser;
import org.w3c.css.css.StyleReport;
import org.w3c.css.css.StyleReportFactory;
import org.w3c.css.css.StyleSheet;
import org.w3c.css.css.StyleSheetParser;
import org.w3c.css.css.TagSoupStyleSheetHandler;
import org.w3c.css.error.ErrorReport;
import org.w3c.css.error.ErrorReportFactory;
import org.w3c.css.index.IndexGenerator;
import org.w3c.css.properties.PropertiesLoader;
import org.w3c.css.util.ApplContext;
import org.w3c.css.util.Codecs;
import org.w3c.css.util.FakeFile;
import org.w3c.css.util.HTTPURL;
import org.w3c.css.util.NVPair;
import org.w3c.css.util.Utf8Properties;
import org.w3c.css.util.Util;
import org.w3c.www.mime.MimeType;
import org.w3c.www.mime.MimeTypeFormatException;
/**
* This class is a servlet to use the validator.
*
* @version $Revision: 1.42 $
*/
public final class CssValidator extends HttpServlet {
final static String texthtml = "text/html";
final static String applxhtml = "application/xhtml+xml";
final static String textplain = "text/plain";
final static String textcss = "text/css";
final static String textunknwon = "text/unknown";
final static String soap12 = "application/soap+xml";
final static String json = "application/json";
final static String server_name = "Jigsaw/2.2.5 "
+ "W3C_CSS_Validator_JFouffa/2.0";
final static String headers_name = "X-W3C-Validator-";
/**
* Create a new CssValidator.
*/
public CssValidator() {
}
/**
* Initializes the servlet and logs the initialization. The init method is
* called once, automatically, by the network service each time it loads
* the servlet. It is guaranteed to finish before any service requests are
* accepted. On fatal initialization errors, an UnavailableException should
* be thrown. Do not call the method System.exit.
*
* <p>
* The init method stores the ServletConfig object. Servlet writers who
* specialize this method should call either super.init, or store the
* ServletConfig object themselves. If an implementor decides to store the
* ServletConfig object in a different location, then the getServletConfig
* method must also be overridden.
*
* <P>
* <DL>
* <STRONG>Init parameters:</STRONG>
* <DT>debug
* <DD><code>true</code> if you want to be in debug mode.
* <DT>aural
* <DD><code>true</code> if you want to be in aural mode.
* <DT>import
* <DD><code>false</code> if you don't want to activate the import
* statement. For security reasons, you shoud be careful when you lunch the
* servlet on a HTTP server with special access authorization.
* <DT>input
* <DD><code>html</code> if the user have an HTML input or
* <code>xml</code> otherwise. <strong>deprecated</strong>
* </DL>
*
* @param config
* servlet configuration information.
* @exception ServletException
* if a servlet exception has occurred.
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// [SECURITY] don't forget this !
Util.servlet = true;
if (config.getInitParameter("debug") != null) {
// servlet debug mode
// define a boolean property CSS.StyleSheet.debug if you want more
// debug.
Util.onDebug = config.getInitParameter("debug").equals("true");
System.err.println("RUN IN DEBUG MODE: "
+ config.getInitParameter("debug").equals("true"));
} else if (Util.onDebug) {
System.err.println("RUN IN DEBUG MODE"+
" but activated outside the servlet");
}
if ((config.getInitParameter("import") != null)
&& (config.getInitParameter("import").equals("false"))) {
Util.importSecurity = true;
}
// The following code will check if the index files are missing or outdated
// If so, the files will be regenerated
// This is done in a Thread so that the validation can carry on.
new Thread () {
public void run () {
IndexGenerator.generatesIndex(true);
}
}.start();
}
private PrintWriter getLocalPrintWriter(OutputStream os, String encoding)
throws IOException {
if (encoding != null) {
return new PrintWriter(new OutputStreamWriter(os, encoding));
} else {
return new PrintWriter(new OutputStreamWriter(os,
Utf8Properties.ENCODING));
}
}
/**
* Performs the HTTP GET operation.
* An HTTP BAD_REQUEST error is reported if
* an error occurs. This servlet writers shouldn't set the headers for the
* requested entity (content type and encoding).
*
* <P>
* Note that the GET operation is expected to be <em>safe</em>, without
* any side effects for which users might be held responsible. For example,
* most form queries have no side effects. Requests intended to change
* stored data should use some other HTTP method. (There have been cases of
* significant security breaches reported because web-based applications
* used GET inappropriately.)
*
* <P>
* The GET operation is also expected to be <em>idempotent</em>, meaning
* that it can safely be repeated. This is not quite the same as being
* safe, but in some common examples the requirements have the same result.
* For example, repeating queries is both safe and idempotent
* (unless payment is required!), but buying something or modifying data
* is neither safe nor idempotent.
*
* <P>
* <DL>
* <STRONG>Forms parameters:</STRONG>
* <DT>URL
* <DD>the URL to be parsed.
* <DT>submitURL
* <DD>if the user want to parse an URL.
* <DT>text
* <DD>The text to be parsed.
* <DT>submitTEXT
* <DD>if the user want to parse the text.
* <DT>output
* <DD>HTML if the user want an HTML output or XML otherwise.
* <DT>input
* <DD>HTML if the user have an HTML input or XML otherwise.
* </DL>
*
* @param req
* encapsulates the request to the servlet.
* @param resp
* encapsulates the response from the servlet.
* @exception ServletException
* if the request could not be handled.
* @exception IOException
* if detected when handling the request.
* @see org.w3c.css.css.StyleSheetGenerator
*/
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
boolean errorReport = true;
int warningLevel = 2;
CssParser parser = null;
String lang = null;
try {
lang = req.getParameter("lang");
}
catch(Exception e) {
lang = null;
}
if(lang == null || lang.equals("")) {
lang = req.getHeader("Accept-Language");
}
else {
lang += ',' + req.getHeader("Accept-Language");
}
ApplContext ac = new ApplContext(lang);
ac.setLink(req.getQueryString());
ac.setContentEncoding(req.getHeader("Accept-Charset"));
String output = req.getParameter("output");
String uri = null;
try {
uri = req.getParameter("uri"); // null if the parameter does not
// exist
} catch (Exception ex) {
// pb in URI decoding (bad escaping, most probably)
handleError(res, ac, output, "No file", new IOException(
"Invalid escape sequence in URI"), false);
}
String text = null;
try {
text = req.getParameter("text");
} catch (Exception ex) {
// pb in URI decoding (bad escaping, most probably)
// not sure it will work here, as it may be catched by the first
// getParameter call
handleError(res, ac, output, "Invalid text", new IOException(
"Invalid escape sequence in URI"), false);
}
String warning = req.getParameter("warning");
String error = req.getParameter("error");
String profile = req.getParameter("profile");
String usermedium = req.getParameter("usermedium");
String type = req.getParameter("type");
if (type == null)
type = "none";
String credential = req.getHeader("Authorization");
if ((credential != null) && (credential.length() > 1)) {
ac.setCredential(credential);
}
if (usermedium == null || "".equals(usermedium)) {
usermedium = "all";
}
InputStream in = req.getInputStream();
ac.setMedium(usermedium);
if (req.getParameter("debug") != null) {
Util.onDebug = req.getParameter("debug").equals("true");
if (Util.onDebug) {
System.err.println("SWITCH DEBUG MODE REQUEST");
}
}
else {
Util.onDebug = false;
}
//text = Util.suppressWhiteSpace(text);
uri = Util.suppressWhiteSpace(uri);
if (output == null) {
output = texthtml;
}
// CSS version
if (profile != null && (!"none".equals(profile) || "".equals(profile))) {
if ("css1".equals(profile) || "css2".equals(profile)
|| "css21".equals(profile)
|| "css3".equals(profile) || "svg".equals(profile)
|| "svgbasic".equals(profile) || "svgtiny".equals(profile)) {
ac.setCssVersion(profile);
} else {
ac.setProfile(profile);
ac.setCssVersion(PropertiesLoader.config.getProperty("defaultProfile"));
}
} else {
ac.setProfile("none");
ac.setCssVersion(PropertiesLoader.config.getProperty("defaultProfile"));
}
if (Util.onDebug) {
System.err.println("[DEBUG] profile is : " + ac.getCssVersion()
+ " medium is " + usermedium);
}
// verify the request
if ((uri == null) && (text == null)) {
// res.sendError(res.SC_BAD_REQUEST,
// "You have send an invalid request.");
handleError(res, ac, output, "No file",
new IOException(ac.getMsg().getServletString("invalid-request")),
false);
return;
}
in.close();
// set the warning output
if (warning != null) {
if (warning.equals("no")) {
warningLevel = -1;
} else {
try {
warningLevel = Integer.parseInt(warning);
} catch (Exception e) {
System.err.println(e);
}
}
ac.setWarningLevel(warningLevel);
}
// set the error report
if (error != null && error.equals("no")) {
errorReport = false;
}
// debug mode
Util.verbose("\nServlet request ");
if (uri != null) {
Util.verbose("Source file : " + uri);
} else {
Util.verbose("TEXTAREA Input");
}
// verbose("From " + req.getRemoteHost() +
// " (" + req.getRemoteAddr() + ") at " + (new Date()) );
if (uri != null) {
// HTML document
try {
uri = HTTPURL.getURL(uri).toString(); // needed to be sure
// that it is a valid
// url
uri = uri.replaceAll(" ", "%20");
DocumentParser URLparser = new DocumentParser(ac, uri);
handleRequest(ac, res, uri, URLparser.getStyleSheet(), output,
warningLevel, errorReport);
} catch (ProtocolException pex) {
if (Util.onDebug) {
pex.printStackTrace();
}
res.setHeader("WWW-Authenticate", pex.getMessage());
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (Exception e) {
handleError(res, ac, output, uri, e, true);
}
} else if (text != null) {
String fileName = "TextArea";
Util.verbose("- " + fileName + " Data -");
Util.verbose(text);
Util.verbose("- End of " + fileName + " Data");
InputStream is = new ByteArrayInputStream(text.getBytes());
fileName = "file://localhost/" + fileName;
try {
if ("css".equals(type) || ( "none".equals(type) && isCSS(text))) {
// if CSS:
parser = new StyleSheetParser();
parser.parseStyleElement(ac, is, null, usermedium,
new URL(fileName), 0);
handleRequest(ac, res, fileName, parser
.getStyleSheet(), output, warningLevel, errorReport);
} else {
// else, trying HTML
TagSoupStyleSheetHandler handler = new TagSoupStyleSheetHandler(null, ac);
handler.parse(is, fileName);
handleRequest(ac, res, fileName, handler.getStyleSheet(), output,
warningLevel, errorReport);
}
} catch (ProtocolException pex) {
if (Util.onDebug) {
pex.printStackTrace();
}
res.setHeader("WWW-Authenticate", pex.getMessage());
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (Exception e) {
handleError(res, ac, output, fileName, e, false);
}
}
Util.verbose("CssValidator: Request terminated.\n");
}
/**
* This method is used for the direct input
* If the <style> tag is found, it may be an HTML entry
* The exception is when this tag is inside comment
* It might also be an HTML document with no CSS => why ?
* Or with only imports (we can't chack thoses imports...)
* @param text, the textarea to test
* @return <tt>false</tt> if it contains the style tag well formed
*/
private boolean isCSS(String text) {
try {
text = text.toLowerCase();
int p = text.indexOf("<style");
return p == -1 || p > text.indexOf("</style>");
} catch (Exception e) {
System.err.println("error: " + e.getMessage());
return true;
}
}
/**
* Performs the HTTP POST operation. An HTTP BAD_REQUEST error is reported
* if an error occurs. The headers that are set should include content type,
* length, and encoding. Setting content length allows the servlet to take
* advantage of HTTP "connection keep alive". If content length can not be
* set in advance, the performance penalties associated with not using keep
* alives will sometimes be avoided if the response entity fits in an
* internal buffer. The servlet implementor must write the headers before
* the response data because the headers can be flushed at any time after
* the data starts to be written.
*
* <P>
* This method does not need to be either "safe" or "idempotent". Operations
* requested through POST could be ones for which users need to be held
* accountable. Specific examples including updating stored data or buying
* things online.
*
* <P>
* <DL>
* <STRONG>Forms parameters:</STRONG>
* <DT>file
* <DD>The input file to be parsed.
* <DT>output
* <DD>The format output.
* <DT>input
* <DD>HTML if the user have an HTML input or XML otherwise.
* </DL>
*
* @param req
* encapsulates the request to the servlet
* @param resp
* encapsulates the response from the servlet
* @exception ServletException
* if the request could not be handled
* @exception IOException
* if detected when handling the request
* @see org.w3c.css.css.StyleSheetGenerator
*/
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
String lang = null;
try {
lang = req.getParameter("lang");
}
catch(Exception e) {
lang = null;
}
boolean errorReport = true;
int warningLevel = 2;
CssParser parser = null;
FakeFile file = null;
String text = null;
String output = null;
//boolean XMLinput = false;
String warning = null;
String error = null;
String profile = "none";
String usermedium = "all";
ServletInputStream in = req.getInputStream();
byte[] buf = new byte[2048];
byte[] general = new byte[65536];
int count = 0;
int len;
if (req.getParameter("debug") != null) {
Util.onDebug = req.getParameter("debug").equals("true");
if (Util.onDebug) {
System.err.println("SWITCH DEBUG MODE REQUEST");
}
}
else {
Util.onDebug = false;
}
Util.verbose("\nCssValidator: Servlet request ");
// verbose("From " + req.getRemoteHost() +
// " (" + req.getRemoteAddr() + ") at " + (new Date()) );
Util.verbose("Content-length : " + req.getContentLength());
if (req.getContentType().trim().startsWith("multipart/form-data")) {
Util.verbose("Content-type : multipart/form-data");
}
try {
while ((len = in.readLine(buf, 0, buf.length)) != -1) {
if (len >= 2 && buf[len - 1] == '\n' && buf[len - 2] == '\r') {
len -= 1;
buf[len - 1] = (byte) '\n';
}
if (len != 0 && buf[len - 1] == '\r') {
buf[len - 1] = (byte) '\n';
}
if (general.length < (count + len)) {
byte[] old = general;
general = new byte[old.length * 2];
System.arraycopy(old, 0, general, 0, old.length);
}
System.arraycopy(buf, 0, general, count, len);
count += len;
}
} finally {
in.close();
}
try {
buf = new byte[count];
System.arraycopy(general, 0, buf, 0, count);
NVPair[] tmp = Codecs.mpFormDataDecode(buf, req.getContentType());
for (int i = 0; i < tmp.length; i++) {
if (tmp[i].getName().equals("file")) {
file = (FakeFile) tmp[i].getValue();
} else if (tmp[i].getName().equals("text")) {
text = (String) tmp[i].getValue();
} else if (tmp[i].getName().equals("lang")) {
lang = (String) tmp[i].getValue();
} else if (tmp[i].getName().equals("output")) {
output = (String) tmp[i].getValue();
} else if (tmp[i].getName().equals("warning")) {
warning = (String) tmp[i].getValue();
} else if (tmp[i].getName().equals("error")) {
warning = (String) tmp[i].getValue();
//} else if (tmp[i].getName().equals("input")) {
// XMLinput = ((String) tmp[i].getValue()).equals("XML");
} else if (tmp[i].getName().equals("profile")) {
profile = (String) tmp[i].getValue();
} else if (tmp[i].getName().equals("usermedium")) {
usermedium = (String) tmp[i].getValue();
if (usermedium == null || "".equals(usermedium)) {
usermedium = "all";
}
}
}
} catch (Exception e) {
System.out.println("Oups! Error in Util/Codecs.java?!?");
e.printStackTrace();
}
if(lang == null || lang.equals("")) {
lang = req.getHeader("Accept-Language");
}
else {
lang += ',' + req.getHeader("Accept-Language");
}
ApplContext ac = new ApplContext(lang);
ac.setLink(req.getQueryString());
ac.setMedium(usermedium);
if (output == null) {
output = texthtml;
}
if ((file == null || file.getSize() == 0) &&
(text == null || text.length() == 0)) {
// res.sendError(res.SC_BAD_REQUEST,
// "You have send an invalid request");
handleError(res, ac, output, "No file",
new IOException(ac.getMsg().getServletString(
"invalid-request")),
false);
return;
}
// set the warning output
if (warning != null) {
if (warning.equals("no")) {
warningLevel = -1;
} else {
try {
warningLevel = Integer.parseInt(warning);
} catch (Exception e) {
System.err.println(e);
}
}
ac.setWarningLevel(warningLevel);
}
// set the error report
if (error != null && error.equals("no")) {
errorReport = false;
}
// CSS version
if (profile != null && (!"none".equals(profile) ||"".equals(profile))) {
if ("css1".equals(profile) || "css2".equals(profile)
|| "css21".equals(profile)
|| "css3".equals(profile) || "svg".equals(profile)
|| "svgbasic".equals(profile) || "svgtiny".equals(profile)) {
ac.setCssVersion(profile);
} else {
ac.setProfile(profile);
ac.setCssVersion(PropertiesLoader.config.getProperty(
"defaultProfile"));
}
} else {
ac.setProfile("none");
ac.setCssVersion(PropertiesLoader.config.getProperty(
"defaultProfile"));
}
String fileName = "";
InputStream is = null;
boolean isCSS = false;
if (file != null && file.getSize() != 0) {
ac.setFakeFile(file);
fileName = file.getName();
Util.verbose("File : " + fileName);
// another way to get file type...
isCSS = file.getContentType().equals(textcss);
} else if (text != null ) {
ac.setFakeText(text);
fileName = "TextArea";
Util.verbose("- " + fileName + " Data -");
Util.verbose(text);
Util.verbose("- End of " + fileName + " Data");
//quick test that works in most cases to determine wether it's
//HTML or CSS
isCSS = isCSS(text);
}
fileName = "file://localhost/" + fileName;
try {
URL u = new URL(fileName);
is = ac.getFakeInputStream(u);
ac.setFakeURL(fileName);
if (isCSS) {
//if CSS:
parser = new StyleSheetParser();
parser.parseStyleElement(ac, is, null, usermedium,
new URL(fileName), 0);
handleRequest(ac, res, fileName, parser.getStyleSheet(),
output, warningLevel, errorReport);
} else {
// else, trying HTML
TagSoupStyleSheetHandler handler;
handler = new TagSoupStyleSheetHandler(null, ac);
handler.parse(is, fileName);
handleRequest(ac, res, fileName, handler.getStyleSheet(),
output, warningLevel, errorReport);
}
} catch (ProtocolException pex) {
if (Util.onDebug) {
//pex.printStackTrace();
}
res.setHeader("WWW-Authenticate", pex.getMessage());
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (Exception e) {
handleError(res, ac, output, fileName, e, false);
}
Util.verbose("CssValidator: Request terminated.\n");
}
private void handleRequest(ApplContext ac, HttpServletResponse res,
String title, StyleSheet styleSheet,
String output, int warningLevel,
boolean errorReport) throws Exception {
buildHeader(ac, res, output);
if (styleSheet == null) {
throw new IOException(ac.getMsg().getServletString("process") + " "
+ title);
}
// if the output parameter was a mime type, we convert it
// to an understandable value for the StyleReportFactory
if ("text/xml".equals(ac.getInput()) && texthtml.equals(output)) {
output = "xhtml";
} else if (texthtml.equals(output)) {
output = "html";
} else if (soap12.equals(output)) {
output = "soap12";
} else if (json.equals(output)) {
output = "json";
} else if(textplain.equals(output)) {
output = "text";
}
styleSheet.findConflicts(ac);
StyleReport style = StyleReportFactory.getStyleReport(ac, title,
styleSheet, output, warningLevel);
if (!errorReport) {
style.desactivateError();
}
PrintWriter out = getLocalPrintWriter(res.getOutputStream(), ac
.getContentEncoding());
int nb_errors = styleSheet.getErrors().getErrorCount();
res.setHeader(headers_name + "Errors", String.valueOf(nb_errors));
res.setHeader(headers_name + "Status", nb_errors == 0 ? "Valid" : "Invalid");
try {
style.print(out);
} finally {
out.close();
}
}
/**
* Generates the response header
* @param ac
* @param res
* @param output
* @throws MimeTypeFormatException
*/
private void buildHeader(ApplContext ac, HttpServletResponse res,
String output)
{
// I don't want cache for the response (inhibits proxy)
res.setHeader("Pragma", "no-cache"); // @@deprecated
res.setHeader("Cache-Control", "no-cache");
// Here is a little joke :-)
// res.setHeader("Server", server_name);
if(output == null) {
output = new String(texthtml);
}
// set the content-type for the response
MimeType outputMt = null;
if (output.equals(texthtml) || output.equals("html")) {
outputMt = MimeType.TEXT_HTML.getClone();
} else if (output.equals(applxhtml) || output.equals("xhtml")) {
outputMt = MimeType.APPLICATION_XHTML_XML.getClone();
} else if (output.equals(soap12) || output.equals("soap12")) {
// invert the comments on the following lines to (de)activate
// the soap Mime Type
try {
outputMt = new MimeType(soap12);
}
catch (MimeTypeFormatException e) {
outputMt = MimeType.TEXT_PLAIN.getClone();
}
//outputMt = MimeType.TEXT_PLAIN.getClone();
} else if(output.equals("ucn")) {
outputMt = MimeType.APPLICATION_XML.getClone();
} else if(output.equals("json")) {
try {
outputMt = new MimeType(json);
}
catch (MimeTypeFormatException e) {
outputMt = MimeType.TEXT_PLAIN.getClone();
}
} else {
// Change this line if you want text/html output when incorrect
// output is passed
outputMt = MimeType.TEXT_PLAIN.getClone();
}
if(ac != null) {
// ignore content encoding if output is SOAP
if(output.equals("soap12")) {
ac.setContentEncoding(null);
}
if (ac.getContentEncoding() != null) {
outputMt.setParameter("charset", ac.getContentEncoding());
}
res.setContentType(outputMt.toString());
if (ac.getContentLanguage() != null) {
res.setHeader("Content-Language", ac.getContentLanguage());
} else {
res.setHeader("Content-Language", "en");
}
}
else {
res.setHeader("Content-Language", "en");
res.setHeader("charset", Utf8Properties.ENCODING);
}
res.setHeader("Vary", "Accept-Language");
}
private void handleError(HttpServletResponse res, ApplContext ac,
String output, String title, Exception e,
boolean validURI)
throws IOException {
System.err.println("[ERROR VALIDATOR] " + title);
System.err.println(e.toString());
e.printStackTrace();
buildHeader(ac, res, output);
res.setStatus(500);
if((e instanceof java.net.UnknownHostException) ||
((e instanceof java.io.FileNotFoundException) &&
((e.getMessage().indexOf("Not Found") != -1) ||
(e.getMessage().indexOf("Service Unavailable") != -1)))) {
validURI = true;
}
else {
validURI = false;
}
PrintWriter out = getLocalPrintWriter(res.getOutputStream(), ac
.getContentEncoding());
ErrorReport error = ErrorReportFactory.getErrorReport(ac, title, output,
e, validURI);
res.setHeader(headers_name + "Status", "Abort");
try {
error.print(out);
}
finally {
out.close();
}
}
}