/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.gui.control.generic.iframe;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.olat.core.dispatcher.impl.StaticMediaDispatcher;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.gui.components.htmlheader.jscss.CustomCSSDelegate;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.NotFoundMediaResource;
import org.olat.core.gui.media.StringMediaResource;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.SimpleHtmlParser;
import org.olat.core.util.StringHelper;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSMediaResource;
/**
*
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*/
public class IFrameDeliveryMapper implements Mapper {
private static final OLog log = Tracing.createLoggerFor(IFrameDeliveryMapper.class);
private static final String DEFAULT_ENCODING = "iso-8859-1";
private static final String UNICODE_ENCODING = "unicode";
private static final String DEFAULT_CONTENT_TYPE = "text/html";
private static final String XHTML_EXTENSION = "xhtml";
private static final String XHTML_CONTENT_TYPE = "application/xhtml+xml";
private static final Pattern PATTERN_ENCTYPE = Pattern.compile("<meta[^>]*charset=[\"\']?([A-Za-z0-9.:\\-_]*)", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_XML_ENCTYPE = Pattern.compile("<\\?xml.*encoding=[\"\']([^\"\']*)[\"\']", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_CONTTYPE = Pattern.compile("<meta.*content-type\"?\\s*content\\s*=\\s*[\"]?+(.+?)([\"]?+\\s*/>)", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_DOCTYPE = Pattern.compile("<!DOCTYPE\\s*html\\s*PUBLIC\\s*[\"\']\\s*-//W3C//DTD\\s*(.+?)(//EN)", Pattern.CASE_INSENSITIVE);
private static final String FILE_SUFFIX_HTM = "htm";
private static final String TAG_FRAMESET = "<frameset";
private static final String TAG_FRAMESET_UPPERC = "<FRAMESET";
private static final String FILE_SUFFIX_JS = ".js";
private VFSItem rootDir;
private boolean rawContent;
private boolean enableTextmarking;
private boolean adjusteightAutomatically;
private String jsEncoding;
private String contentEncoding;
private String frameId;
private String customCssURL;
private transient CustomCSSDelegate customCssDelegate;
private String themeBaseUri;
private String customHeaderContent;
private Boolean jQueryEnabled;
private Boolean prototypeEnabled;
private Boolean openolatCss;
private transient boolean checkForInlineEvent;
private transient long suppressEndlessReload;
public IFrameDeliveryMapper() {
//for XStream
}
public IFrameDeliveryMapper(VFSItem rootDir, boolean rawContent, boolean enableTextmarking, boolean adjusteightAutomatically,
String frameId, String customCssURL, String themeBaseUri, String customHeaderContent) {
this.rootDir = rootDir;
this.rawContent = rawContent;
this.enableTextmarking = enableTextmarking;
this.adjusteightAutomatically = adjusteightAutomatically;
this.frameId = frameId;
this.customCssURL = customCssURL;
this.themeBaseUri = themeBaseUri;
this.customHeaderContent = customHeaderContent;
}
public void setDeliveryOptions(DeliveryOptions config) {
if(config != null) {
Boolean standard = config.getStandardMode();
if(standard != null && standard.booleanValue()) {
rawContent = true;
openolatCss = false;
jQueryEnabled = false;
prototypeEnabled = false;
enableTextmarking = false;
adjusteightAutomatically = false;
} else {
jQueryEnabled = config.getjQueryEnabled();
prototypeEnabled = config.getPrototypeEnabled();
if(config.getGlossaryEnabled() != null) {
enableTextmarking = config.getGlossaryEnabled().booleanValue();
}
openolatCss = config.getOpenolatCss();
if(DeliveryOptions.CONFIG_HEIGHT_AUTO.equals(config.getHeight())) {
adjusteightAutomatically = true;
} else if(StringHelper.containsNonWhitespace(config.getHeight())) {
adjusteightAutomatically = false;
}
}
if(config.getContentEncoding() != null) {
contentEncoding = config.getContentEncoding();
}
if(config.getJavascriptEncoding() != null) {
jsEncoding = config.getJavascriptEncoding();
}
}
}
public void setCheckForInlineEvent(boolean checkForInlineEvent) {
this.checkForInlineEvent = checkForInlineEvent;
}
public void setAdjusteightAutomatically(boolean adjusteightAutomatically) {
this.adjusteightAutomatically = adjusteightAutomatically;
}
public void setEnableTextmarking(boolean enableTextmarking) {
this.enableTextmarking = enableTextmarking;
}
public void setRawContent(boolean rawContent) {
this.rawContent = rawContent;
}
public void setJsEncoding(String jsEncoding) {
this.jsEncoding = jsEncoding;
}
public void setContentEncoding(String contentEncoding) {
this.contentEncoding = contentEncoding;
}
public void setCustomHeaderContent(String customHeaderContent) {
this.customHeaderContent = customHeaderContent;
}
public void setCustomCssURL(String customCssURL) {
this.customCssURL = customCssURL;
}
public void setCustomCssDelegate(CustomCSSDelegate customCssDelegate) {
this.customCssDelegate = customCssDelegate;
if(customCssDelegate.getCustomCSS() != null) {
customCssURL = customCssDelegate.getCustomCSS().getCSSURLIFrame();
}
}
@Override
public MediaResource handle(String relPath, HttpServletRequest request) {
String isPopUpParam = request.getParameter("olatraw");
boolean isPopUp = false;
if (isPopUpParam != null && isPopUpParam.equals("true")) {
isPopUp = true;
}
return deliverFile(request, relPath, isPopUp);
}
protected MediaResource deliverFile(HttpServletRequest httpRequest, String path, boolean isPopUp) {
MediaResource mr;
VFSLeaf vfsLeaf = null;
VFSItem vfsItem = null;
//if directory gets renamed root becomes null
if (rootDir == null) {
return new NotFoundMediaResource("directory not found"+path);
} else {
vfsItem = rootDir.resolve(path);
}
//only files are allowed, but somehow it happened that folders showed up here
if (vfsItem instanceof VFSLeaf) {
vfsLeaf = (VFSLeaf) rootDir.resolve(path);
} else {
mr = new NotFoundMediaResource(path);
}
if (vfsLeaf == null) {
mr = new NotFoundMediaResource(path);
} else {
// check if path ends with .html, .htm or .xhtml. We do this by searching for "htm"
// and accept positions of this string at length-3 or length-4
if (path.toLowerCase().lastIndexOf(FILE_SUFFIX_HTM) >= (path.length()-4)) {
// set the http content-type and the encoding
Page page = loadPageWithGuess(vfsLeaf);
String pageEncoding = page.getEncoding();
if (page.isUseLoadedPageString()) {
mr = prepareMediaResource(httpRequest, page.getPage(), pageEncoding, page.getContentType(), isPopUp);
} else {
// found a new charset other than iso-8859-1, load string with proper encoding
String content = FileUtils.load(vfsLeaf.getInputStream(), pageEncoding);
mr = prepareMediaResource(httpRequest, content, pageEncoding, page.getContentType(), isPopUp);
}
if(contentEncoding == null) {
contentEncoding = pageEncoding;
}
} else if (path.endsWith(FILE_SUFFIX_JS)) { // a javascript library
VFSMediaResource vmr = new VFSMediaResource(vfsLeaf);
// set the encoding; could be null if this page starts with .js file
// (not very common...).
// if we set no header here, apache sends the default encoding
// together with the mime-type, which is wrong.
// so we assume the .js file has the same encoding as the html file
// that loads the .js file
if (jsEncoding != null) {
vmr.setEncoding(jsEncoding);
} else if (contentEncoding != null) {
vmr.setEncoding(contentEncoding);
}
mr = vmr;
} else {
// binary data: not .html, not .htm, not .js -> treated as is
VFSMediaResource vmr = new VFSMediaResource(vfsLeaf);
String filename = vfsLeaf.getName();
// This is to prevent the login prompt in Excel, Word and PowerPoint
if(filename.endsWith(".xlsx") || filename.endsWith(".pptx") || filename.endsWith(".docx")) {
vmr.setDownloadable(true);
}
mr = vmr;
}
}
return mr;
}
private StringMediaResource prepareMediaResource(HttpServletRequest httpRequest, String page, String enc, String contentType, boolean isPopUp) {
StringMediaResource smr = new StringMediaResource();
if(XHTML_CONTENT_TYPE.equals(contentType)) {
//check if the application/xhtml+xml is supported (not supported by IEs)
//if not, replace the content type by text/html for compatibility
String accept = httpRequest.getHeader("Accept");
if(accept == null || accept.indexOf(XHTML_CONTENT_TYPE) < 0) {
contentType = DEFAULT_CONTENT_TYPE;
}
}
String mimetype = contentType + ";charset=" + StringHelper.check4xMacRoman(enc);
smr.setContentType(mimetype);
smr.setEncoding(enc);
//inject some javascript code to size iframe to proper height, but only when not a page with framesets
if (page.indexOf(TAG_FRAMESET) != -1 || page.indexOf(TAG_FRAMESET_UPPERC) != -1 || isPopUp) {
//is frameset -> deliver unparsed
smr.setData(page);
} else {
String agent = httpRequest.getHeader("User-Agent");
boolean firefoxWorkaround = agent != null && agent.indexOf("Firefox/") > 0;
if(rawContent) {
smr.setData(page);
} else {
smr.setData(injectJavaScript(page, mimetype, checkForInlineEvent, firefoxWorkaround));
}
// When loading next page, check if it was an inline user click
this.checkForInlineEvent = true;
}
return smr;
}
/**
* it would be possible to access the iframe.document but there is no event
* sended when the content changes. Like this is is easier to inject the js
* code and resize the iframe like this.
*
* @param page
* @param addCheckForInlineEvents
* true: check if page is rendered in iframe, if yes send event
* to framework; false: don't do this check
* @return
*/
/**
* TODO:gs make more stable by only adding some js stuff to the end of the page. First check if document.height is ready
* when puttings js to the end or menachism like ext.onReady is needed
*/
private String injectJavaScript(String page, String mimetype, boolean addCheckForInlineEvents, boolean anchorFirefoxWorkaround) {
//do not use parser and just check for css and script stuff myself and append just before body and head
SimpleHtmlParser parser = new SimpleHtmlParser(page);
if (!parser.isValidHtml()) {
return page;
}
String docType = parser.getHtmlDocType();
HtmlOutput sb = new HtmlOutput(docType, themeBaseUri, page.length() + 1000);
if (docType != null) sb.append(docType).append("\n");
if (parser.getXhtmlNamespaces() == null) sb.append("<html><head>");
else {
sb.append(parser.getXhtmlNamespaces());
sb.append("<head>\n<meta http-equiv=\"Content-Script-Type\" content=\"text/javascript\"/>");//neded to allow body onload attribute
}
//<meta http-equiv="content-type" content="text/html; charset=utf-8" />
sb.append("\n<meta http-equiv=\"content-type\" content=\"").append(mimetype).append("\"");
if (docType != null && docType.indexOf("XHTML") > 0) sb.append("/"); // close tag only when xhtml to validate
sb.append(">");
if(openolatCss != null && openolatCss.booleanValue()) {
sb.appendOpenolatCss();
}
if(!parser.hasOwnCss()) {
if(openolatCss == null || openolatCss.booleanValue()) {
//add olat content css as used in html editor
sb.appendOpenolatCss();//css only loaded once in HtmlOutput
}
if(customCssDelegate != null && customCssDelegate.getCustomCSS() != null
&& customCssDelegate.getCustomCSS().getCSSURLIFrame() != null) {
String customCssURL = customCssDelegate.getCustomCSS().getCSSURLIFrame();
sb.appendCss(customCssURL, "customcss");
} else if (customCssURL != null) {
// add the custom CSS, e.g. the course css that overrides the standard content css
sb.appendCss(customCssURL, "customcss");
}
}
if (enableTextmarking) {
if (log.isDebug()) {
log.debug("Textmarking is enabled, including tooltips js files into iframe source...");
}
sb.appendJQuery();
sb.appendGlossary();
}
if(jQueryEnabled != null && jQueryEnabled.booleanValue()) {
sb.appendJQuery();
}
if(prototypeEnabled != null && prototypeEnabled.booleanValue()) {
sb.appendPrototype();
}
// Load some iframe.js helper code
sb.append("\n<script type=\"text/javascript\">\n/* <![CDATA[ */\n");
// Set the iframe id, used by the resize function. Important to set before iframe.js is loaded
sb.append("b_iframeid=\"").append(frameId).append("\";");
sb.append("b_isInlineUri=").append(Boolean.valueOf(addCheckForInlineEvents).toString()).append(";");
sb.append("\n/* ]]> */\n</script>");
sb.appendStaticJs("js/openolat/iframe.js");
// Resize frame to fit height of html page.
// Do this only when there is some content available. This can be false when
// the content is written all dynamically via javascript. In this cases, the
// resizeing is meaningless anyway.
if (parser.getHtmlContent().length() > 0) {
sb.append("\n<script type=\"text/javascript\">\n/* <![CDATA[ */\n");
// register the resize code to be executed on document load and click events
if (adjusteightAutomatically) {
sb.append("b_addOnloadEvent(b_sizeIframe);");
sb.append("b_addOnclickEvent(b_sizeIframe);");
}
// register the tooltips enabling on document load event
sb.append("b_addOnloadEvent(b_hideExtMessageBox);");
if (addCheckForInlineEvents) {
// Refresh dirty menu tree by triggering client side request to component which fires events
// which is not possible by mappers. The method will first check if the page is loaded in our
// iframe and ignore all other requests (files in framesets, sub-iframes, AJAX calls etc)
if ((System.currentTimeMillis() - this.suppressEndlessReload) > 2000) sb.append("b_addOnloadEvent(b_sendNewUriEventToParent);");
this.suppressEndlessReload = System.currentTimeMillis();
}
sb.append("b_addOnloadEvent(b_changeLinkTargets);");
if(enableTextmarking){
sb.append("b_addOnloadEvent(b_glossaryHighlight);");
}
if(anchorFirefoxWorkaround) {
sb.append("b_addOnloadEvent(b_anchorFirefoxWorkaround);");
}
sb.append("\n/* ]]> */\n</script>");
}
String origHTMLHead = parser.getHtmlHead();
// jsMath brute force approach to render latex formulas: add library if
// a jsmath class is found in the code and the library is not already in
// the header of the page
if ((page.indexOf("<math") > -1 || page.indexOf("class=\"math\"") != -1 || page.indexOf("class='math'") != -1) && (origHTMLHead == null || origHTMLHead.indexOf("jsMath/easy/load.js") == -1)) {
sb.appendJsMath();
}
// add some custom header things like js code or css
if (customHeaderContent != null) {
sb.append(customHeaderContent);
}
// Add HTML header stuff from original page: css, javascript, title etc.
if (origHTMLHead != null) sb.append(origHTMLHead);
sb.append("\n</head>\n");
// use the original body tag, may include all kind of attributes (class, style, onload, on...)
sb.append(parser.getBodyTag());
// finally add content and finish page
sb.append(parser.getHtmlContent());
sb.append("</body></html>");
return sb.toString();
}
private Page loadPageWithGuess(VFSLeaf vfsPage) {
if(contentEncoding != null && isCharsetSupported(contentEncoding)) {
Page page = new Page();
page.setExtension(FileUtils.getFileSuffix(vfsPage.getName()));
page.setEncoding(contentEncoding);
page.setUseLoadedPageString(true);
String content = FileUtils.load(vfsPage.getInputStream(), contentEncoding);
page.setContentType(guessContentType(page, content));
page.setPage(content);
return page;
}
Page page = new Page();
page.setExtension(FileUtils.getFileSuffix(vfsPage.getName()));
page.setEncoding(DEFAULT_ENCODING);
String content = FileUtils.load(vfsPage.getInputStream(), DEFAULT_ENCODING);
page.setContentType(guessContentType(page, content));
// <meta.*charset=([^"]*)"
//extract only the charset attribute without the overhead of creating an htmlparser
boolean guessed = loadPageWithGuess(page, content, DEFAULT_ENCODING);
if(!guessed) {
//try opening it with utf-8
String contentUnicode = FileUtils.load(vfsPage.getInputStream(), UNICODE_ENCODING);
guessed = loadPageWithGuess(page, contentUnicode, UNICODE_ENCODING);
if(!guessed) {
//take default
page.setPage(content);
page.setUseLoadedPageString(true);
}
}
return page;
}
private boolean loadPageWithGuess(Page page, String content, String encoding) {
//default encoding for xhtml
if(XHTML_CONTENT_TYPE.equals(page.getContentType())) {
page.setEncoding("utf-8");
}
String guessedEncoding = guessEncoding(content);
if (guessedEncoding != null) {
// use found char set
//if longer than 50 the regexp did fail
if (isCharsetSupported(guessedEncoding)) {
page.setEncoding(guessedEncoding);
} else {
return false;
}
// reuse already loaded page when page uses the default encoding
if (page.getEncoding().equalsIgnoreCase(encoding) || page.getEncoding().contains(encoding)
|| page.getEncoding().toLowerCase().contains(encoding)) {
page.setUseLoadedPageString(true);
page.setPage(content);
}
return true;
}
return false;
}
private String guessContentType(Page page, String content) {
String cType = null;
if(XHTML_EXTENSION.equals(page.getExtension())) {
Matcher dm = PATTERN_DOCTYPE.matcher(content);
if (dm.find()) {
String doctype = dm.group(1).toLowerCase();
//default settings for XHTML-documents, should be taken if no <meta http-equiv="content-type" .../> is given
if (doctype.indexOf("xhtml") == 0 && doctype.indexOf("mathml") > 0) {
cType = XHTML_CONTENT_TYPE;
}
}
}
Matcher cm = PATTERN_CONTTYPE.matcher(content);
if (cm.find()) {
//use found content-type
String contentType = cm.group(1);
String[] types=contentType.split(";");
for (int i=0;i<types.length;i++) {
if (!(types[i].contains("charset"))) {
contentType=types[i].trim();
break;
}
}
//if longer than 50 the regexp did fail
if (contentType.length() < 50) {
cType = contentType;
}
}
if(cType == null) {
return DEFAULT_CONTENT_TYPE;
}
if(cType.contains("text/xhtml")) {
//text/xhtml is not accepted as html mime type by most of the browsers
return DEFAULT_CONTENT_TYPE;
}
return cType;
}
protected String guessEncoding(String content) {
Matcher m = PATTERN_ENCTYPE.matcher(content);
if (m.find()) {
// use found char set
String htmlcharset = m.group(1);
//if longer than 50 the regexp did fail
if (htmlcharset.length() < 50 && htmlcharset.length() != 0) {
return htmlcharset;
}
}
Matcher xmlDeclaration = PATTERN_XML_ENCTYPE.matcher(content);
if (xmlDeclaration.find()) {
// use found char set
String xmlcharset = xmlDeclaration.group(1);
//if longer than 50 the regexp did fail
if (xmlcharset.length() < 50 ) {
return xmlcharset;
}
}
return null;
}
private boolean isCharsetSupported(String enc) {
try {
return Charset.isSupported(enc);
} catch (IllegalCharsetNameException e) {
return false;
}
}
private static class HtmlOutput extends StringOutput {
private boolean ooCssLoaded = false;
private boolean jqueryLoaded = false;
private final String docType;
private final String themeBaseUri;
public HtmlOutput(String docType, String themeBaseUri, int length) {
super(length);
this.docType = docType;
this.themeBaseUri = themeBaseUri + "content.css";
}
private void appendOpenolatCss() {
if(ooCssLoaded) return;
appendCss(themeBaseUri, "themecss");
ooCssLoaded = true;
}
public void appendJQuery() {
if(jqueryLoaded) return;
appendJQuery2Cond();
appendStaticJs("js/jshashtable-2.1_src.js");
appendStaticJs("js/jquery/ui/jquery-ui-1.11.4.custom.min.js");
appendStaticCss("js/jquery/ui/jquery-ui-1.11.4.custom.min.css", "jqueryuicss");
jqueryLoaded = true;
}
public void appendJQuery2Cond() {
append("<!--[if lt IE 9]>\n");
appendStaticJs("js/jquery/jquery-1.9.1.min.js");
append("<![endif]-->\n");
append("<!--[if gte IE 9]><!-->\n");
appendStaticJs("js/jquery/jquery-2.1.3.min.js");
append("<!--<![endif]-->\n");
}
public void appendPrototype() {
appendStaticJs("js/prototype/prototype.js");
}
public void appendJsMath() {
append("<script type=\"text/javascript\" src=\"");
append(WebappHelper.getMathJaxCdn());
append("MathJax.js?config=TeX-AMS-MML_HTMLorMML\"></script>\n");
append("<script type=\"text/javascript\">\n");
append("MathJax.Hub.Config({\n");
append(" extensions: [\"jsMath2jax.js\"],\n");
append(" showProcessingMessages: false,\n");
append(" jsMath2jax: {\n");
append(" preview: \"none\"\n");
append(" },\n");
append(" tex2jax: {\n");
append(" ignoreClass: \"math\"\n");
append(" }\n");
append("});");
append("</script>");
}
public void appendGlossary() {
appendStaticJs("js/openolat/glossaryhighlighter.js");
appendStaticJs("js/jquery/ui/jquery-ui-1.11.4.custom.tooltip.min.js");
appendStaticCss("js/openolat/glossaryhighlighter.css", "textmarkercss");
}
public void appendStaticJs(String javascript) {
append("<script type=\"text/javascript\" src=\"");
StaticMediaDispatcher.renderStaticURI(this, javascript);
append("\"></script>\n");
}
public void appendStaticCss(String css, String id) {
append("\n<link rel=\"stylesheet\" type=\"text/css\" id=\"").append(id).append("\" href=\"");
StaticMediaDispatcher.renderStaticURI(this, css);
append("\"");
if (docType != null && docType.indexOf("XHTML") > 0) append("/"); // close tag only when xhtml to validate
append(">\n");
}
public void appendCss(String css, String id) {
append("\n<link rel=\"stylesheet\" type=\"text/css\" id=\"").append(id).append("\" href=\"").append(css).append("\"");
if (docType != null && docType.indexOf("XHTML") > 0) append("/"); // close tag only when xhtml to validate
append(">\n");
}
}
private static class Page {
private String encoding;
private String contentType;
private String extension;
private String page;
private boolean useLoadedPageString = false;
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
public boolean isUseLoadedPageString() {
return useLoadedPageString;
}
public void setUseLoadedPageString(boolean useLoadedPageString) {
this.useLoadedPageString = useLoadedPageString;
}
}
}