package org.wicketstuff.htmlvalidator; import java.io.StringReader; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.wicket.Page; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.core.request.handler.IPageRequestHandler; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.component.IRequestablePage; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.response.filter.IResponseFilter; import org.apache.wicket.util.string.AppendingStringBuffer; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import com.thaiopensource.util.PropertyMapBuilder; import com.thaiopensource.validate.Schema; import com.thaiopensource.validate.ValidateProperty; import com.thaiopensource.validate.Validator; /** * Markup validating response filter that inserts an error report when the * rendered markup is not valid according to the specified DOCTYPE. * * <h3>DOCTYPE</h3> * * The validator tries to determine the markup standard that needs to be * validated based on the DOCTYPE that is defined in the rendered document. See * {@link DocType} for the supported standards. * * When no DOCTYPE is specified, or the DOCTYPE is unknown, this filter will * call {@link #onUnknownDocType(AppendingStringBuffer)}. The default * implementation will log a message in the application log (using level INFO). * * <h3>Valid markup</h3> * * When the rendered markup is valid, an icon will be inserted in the response * markup telling the user that the rendered markup is valid. * * Override {@link #onValidMarkup(AppendingStringBuffer, ValidationReport)} to * change this behavior. * * <h3>Invalid markup</h3> * * When the rendered markup is not valid, a popup will be rendered and presented * to the user, showing the detected standard, page class and the discovered * errors. It also shows the markup with the errors inline. * * Override {@link #onInvalidMarkup(AppendingStringBuffer, ValidationReport)} to * change this default behavior. * * <h3>Configuration</h3> * * There are several {@link HtmlValidationConfiguration configuration options} * available for the validator. The first option is the ability to suppress the * popup of the window when a particular error is discovered. * * <h3>Metadata</h3> * * The filter reports the validation result in the response page's meta data. * You can retrieve the validation result text with the * {@link HtmlValidationResultKey} key. * * <pre> * String result = page.getMetaData(HtmlValidationResultKey.KEY); * </pre> */ public class HtmlValidationResponseFilter implements IResponseFilter { private static final Pattern DOCTYPE_PATTERN = Pattern .compile("<!DOCTYPE[^>]*>"); private static final Logger log = LoggerFactory .getLogger(HtmlValidationResponseFilter.class); private final HtmlValidationConfiguration configuration; /** * Constructs the validator using the default configuration object. You can * access and modify configuration through {@link #getConfiguration()}. */ public HtmlValidationResponseFilter() { this(new HtmlValidationConfiguration()); } /** * Constructs the validator with a custom configuration object. You can * access and modify configuration through {@link #getConfiguration()}. * * @param configuration * a custom configuration object */ public HtmlValidationResponseFilter( HtmlValidationConfiguration configuration) { this.configuration = configuration; } /** * Gets the configuration. * * @return */ public HtmlValidationConfiguration getConfiguration() { return configuration; } /** * Called when the validated markup does not contain any errors. * * @param responseBuffer * the validated response markup * @param report * the validation report */ protected void onValidMarkup(AppendingStringBuffer responseBuffer, ValidationReport report) { IRequestablePage responsePage = getResponsePage(); DocType doctype = getDocType(responseBuffer); log.info("Markup for {} is valid {}", responsePage != null ? responsePage.getClass().getName() : "<unable to determine page class>", doctype.name()); String head = report.getHeadMarkup(); String body = report.getBodyMarkup(); int indexOfHeadClose = responseBuffer.lastIndexOf("</head>"); responseBuffer.insert(indexOfHeadClose, head); int indexOfBodyClose = responseBuffer.lastIndexOf("</body>"); responseBuffer.insert(indexOfBodyClose, body); } /** * Called when the validated markup contains errors. * * @param responseBuffer * the validated response markup * @param report * the validation report containing the errors */ protected void onInvalidMarkup(AppendingStringBuffer responseBuffer, ValidationReport report) { String head = report.getHeadMarkup(); String body = report.getBodyMarkup(); int indexOfHeadClose = responseBuffer.lastIndexOf("</head>"); responseBuffer.insert(indexOfHeadClose, head); int indexOfBodyClose = responseBuffer.lastIndexOf("</body>"); responseBuffer.insert(indexOfBodyClose, body); } /** * Called when no known {@link DocType} could be determined from the * {@code responseBuffer}. The markup is not validated. * * @param responseBuffer * the response markup that could not be validated. */ protected void onUnknownDocType(AppendingStringBuffer response) { IRequestablePage responsePage = getResponsePage(); String detectionString = getFirstCharacters(response, 128); if (responsePage != null) { log.info("No or unknown DOCTYPE detected for page {}: {}", responsePage.getClass().getName(), detectionString); } else { log.info("No or unknown DOCTYPE detected: {}", detectionString); } } public AppendingStringBuffer filter(AppendingStringBuffer responseBuffer) { IRequestablePage responsePage = getResponsePage(); if (responsePage != null && !(responsePage instanceof Page) || RequestCycle.get().find(AjaxRequestTarget.class) != null) { return responseBuffer; } DocType docType = getDocType(responseBuffer); if (docType == null) { setMetaData("Unknown doctype"); onUnknownDocType(responseBuffer); return responseBuffer; } try { ValidationReport report = validateMarkup(responseBuffer.toString(), docType); if (report.isValid()) { setMetaData("Markup is valid " + docType); onValidMarkup(responseBuffer, report); } else { setMetaData("Markup is invalid " + docType); onInvalidMarkup(responseBuffer, report); } } catch (Exception e) { log.error(e.toString(), e); } return responseBuffer; } private ValidationReport validateMarkup(String response, DocType docType) throws Exception { IRequestablePage responsePage = getResponsePage(); ValidationReport report = new ValidationReport(configuration, responsePage, response, docType); PropertyMapBuilder properties = new PropertyMapBuilder(); properties.put(ValidateProperty.ERROR_HANDLER, report); Schema schema = docType.createSchema(); Validator validator = schema .createValidator(properties.toPropertyMap()); XMLReader reader = docType.createParser(); reader.setContentHandler(validator.getContentHandler()); try { reader.parse(new InputSource(new StringReader(response))); } catch (SAXParseException parseError) { report.fatalError(parseError); } return report; } private IRequestablePage getResponsePage() { IRequestHandler requestHandler = RequestCycle.get() .getActiveRequestHandler(); IRequestablePage responsePage = null; if (requestHandler instanceof IPageRequestHandler) { responsePage = ((IPageRequestHandler) requestHandler).getPage(); } return responsePage; } private void setMetaData(String msg) { IRequestablePage responsePage = getResponsePage(); if (responsePage instanceof Page) { ((Page) responsePage).setMetaData(HtmlValidationResultKey.KEY, msg); } } /** * Gets the DOCTYPE from the response. Returns {@code null} when the DOCTYPE * is not detected, or unknown. See {@link DocType} for supported DOCTYPEs. */ protected DocType getDocType(AppendingStringBuffer response) { String contentSoFar = getFirstCharacters(response, 128); Matcher matcher = DOCTYPE_PATTERN.matcher(contentSoFar); if (!matcher.find()) return null; String docTypeStr = matcher.group(); if (Strings.isEmpty(docTypeStr)) return null; DocType docType = DocType.getDocType(docTypeStr); return docType; } private String getFirstCharacters(AppendingStringBuffer response, int max) { int maxLength = Math.min(response.length(), max); String contentSoFar = response.substring(0, maxLength); return contentSoFar; } }