/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <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> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.gui.control.generic.iframe; import java.io.File; import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.Window; import org.olat.core.gui.components.htmlheader.jscss.CustomCSS; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; import org.olat.core.gui.components.panel.Panel; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.control.generic.textmarker.TextMarkerManagerImpl; import org.olat.core.gui.media.MediaResource; import org.olat.core.id.OLATResourceable; import org.olat.core.id.context.BusinessControlFactory; import org.olat.core.id.context.ContextEntry; import org.olat.core.id.context.StateEntry; import org.olat.core.util.CodeHelper; import org.olat.core.util.StringHelper; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.event.MultiUserEvent; import org.olat.core.util.vfs.LocalFolderImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSManager; import org.olat.core.util.vfs.VFSMediaResource; /** * Class that loads a resource (html) in an Iframe and tries to adjust the size of the Iframe to hide the scrollbars. * This is done by injecting some javascript into the head part of the loaded html file which then resizes the iframe itself. * See package documentation for details. * Initial Date: Dec 9, 2004 * * @author Felix Jost<br> * * @author guido */ public class IFrameDisplayController extends BasicController implements GenericEventListener, Activateable2 { private static final String NEW_URI_EVENT = "newUriEvent"; protected static final String FILE_SUFFIX_HTM = "htm"; protected static final String FILE_SUFFIX_JS = ".js"; private static final String COMMAND_DOWNLOAD = "command.download"; private final VelocityContainer myContent = createVelocityContainer("index"); private final VelocityContainer eventVC = createVelocityContainer("event"); private Panel newUriEventPanel; private Panel main; private IFrameDeliveryMapper contentMapper; private DeliveryOptions deliveryOptions; /** * Base URI of contentMapper */ private final String baseURI; /** * Relative uri of currently loaded page in iframe */ private String currentUri; //download link private Link downloadLink; private boolean allowDownload = false; /** * * @param ureq * @param wControl * @param fileRoot File that points to the root directory of the resource */ public IFrameDisplayController(UserRequest ureq, WindowControl wControl, File fileRoot) { this(ureq, wControl, new LocalFolderImpl(fileRoot), null, null); myContent.setDomReplacementWrapperRequired(false); // we provide our own DOM replacement ID } /** * * @param ureq * @param wControl * @param fileRoot * @param ores - send an OLATresourcable of the context (e.g. course) where the iframe runs and it will be checked if the user has textmarking (glossar) enabled in this course */ public IFrameDisplayController(UserRequest ureq, WindowControl wControl, File fileRoot, OLATResourceable ores) { this(ureq, wControl, new LocalFolderImpl(fileRoot), null, ores, null, false, false); } /** * * @param ureq * @param wControl * @param rootDir VFSItem that points to the root folder of the resource */ public IFrameDisplayController(UserRequest ureq, WindowControl wControl, VFSContainer rootDir) { this(ureq, wControl, rootDir, null, null, null, false, false); } /** * * @param ureq * @param wControl * @param rootDir * @param ores - send an OLATresourcable of the context (e.g. course) where the iframe runs and it will be checked if the user has textmarking (glossar) enabled in this course */ public IFrameDisplayController(UserRequest ureq, WindowControl wControl, VFSContainer rootDir, OLATResourceable ores, DeliveryOptions deliveryOptions) { this(ureq, wControl, rootDir, null, ores, deliveryOptions, false, false); } /** * * @param ureq * @param wControl * @param rootDir * @param frameId if you need access to the iframe html id, provide it here * @param enableTextmarking to enable textmakring of the content in the iframe enable it here */ public IFrameDisplayController(final UserRequest ureq, WindowControl wControl, VFSContainer rootDir, String frameId, OLATResourceable contextResourceable, DeliveryOptions options, boolean persistMapper, boolean randomizeMapper) { super(ureq, wControl); //register this object for textMarking on/off events //TODO:gs how to unregister and where? unregister need ureq so dispose does not work if (contextResourceable != null) { ureq.getUserSession().getSingleUserEventCenter().registerFor(this, getIdentity(), contextResourceable); } this.deliveryOptions = options; boolean enableTextmarking = TextMarkerManagerImpl.getInstance().isTextmarkingEnabled(ureq, contextResourceable); // Set correct user content theme String themeBaseUri = wControl.getWindowBackOffice().getWindow().getGuiTheme().getBaseURI(); if (frameId == null) { frameId = "ifdc" + hashCode(); } boolean adjusteightAutomatically = true; //Delivers content files via local mapper to enable session based browser caching for at least this instance //FIXME:FG: implement named mapper concept based on business path to allow browser caching and distributed media server //TODO:gs may use the same contentMapper if users clicks again on the same singlePage, now each time a new Mapper gets created and //therefore the browser can not reuse the cached elements if(persistMapper) { contentMapper = new SerializableIFrameDeliveryMapper(rootDir, false, enableTextmarking, adjusteightAutomatically, frameId, null /*customCssURL*/, themeBaseUri, null /*customHeaderContent*/); } else { contentMapper = new IFrameDeliveryMapper(rootDir, false, enableTextmarking, adjusteightAutomatically, frameId, null /*customCssURL*/, themeBaseUri, null /*customHeaderContent*/); } contentMapper.setDeliveryOptions(options); String mapperID = VFSManager.getRealPath(rootDir); if (mapperID == null) { // can't cache mapper, no cacheable context available baseURI = registerMapper(ureq, contentMapper); } else { // Add classname to the file path to remove conflicts with other // usages of the same file path mapperID = this.getClass().getSimpleName() + ":" + mapperID; if(randomizeMapper) { mapperID += CodeHelper.getRAMUniqueID(); } baseURI = registerCacheableMapper(ureq, mapperID, contentMapper); } myContent.contextPut("baseURI", baseURI); newUriEventPanel = new Panel("newUriEvent"); newUriEventPanel.setContent(eventVC); main = new Panel("iframemain"); main.setContent(myContent); myContent.contextPut("frameId", frameId); myContent.put("newUriEvent", newUriEventPanel); // add default iframe height if(options == null || DeliveryOptions.CONFIG_HEIGHT_AUTO.equals(options.getHeight()) || options.getHeight() == null) { myContent.contextPut("iframeHeight", 600); // used as fallback myContent.contextPut("adjustAutoHeight", Boolean.TRUE); } else { myContent.contextPut("iframeHeight", options.getHeight()); myContent.contextPut("adjustAutoHeight", Boolean.FALSE); } // Add us as cycle listener to be notified when current dispatch cycle is // finished. we then need to add the css which is not yet defined at this // point getWindowControl().getWindowBackOffice().addCycleListener(this); putInitialPanel(main); } /** * Sets the start page, may be null, a relative or an absolute URI * * @param newCurrentURI The currentURI to set */ public void setCurrentURI(String newCurrentURI) { // set new uri and the content dirty to redraw on screen changeCurrentURI(newCurrentURI, true); } /** * Allow download for all types but html * @param allow */ public void setAllowDownload(boolean allow) { this.allowDownload = allow; setPageDownload(isPageDownloadAllowed(currentUri)); } /** * Configuration method to use an explicit height for the iframe instead of * the default automatic sizeing code. If you don't call this method, OLAT * will try to size the iframe so that no scrollbars appear. In most cases * this works. If it does not work, use this method to set an explicit height. * <br /> * Set 0 to reset to automatic behaviour. * * @param height */ public void setHeightPX(int height) { if (height == 0) { myContent.contextPut("iframeHeight", 600); contentMapper.setAdjusteightAutomatically(true); myContent.contextPut("adjustAutoHeight", Boolean.TRUE); } else { myContent.contextPut("iframeHeight", height); contentMapper.setAdjusteightAutomatically(false); myContent.contextPut("adjustAutoHeight", Boolean.FALSE); } } public void setRawContent(boolean rawContent) { contentMapper.setRawContent(rawContent); } public void setContentEncoding(String encoding) { contentMapper.setContentEncoding(encoding); } public void setJSEncoding(String encoding) { contentMapper.setJsEncoding(encoding); } public DeliveryOptions getDeliveryOptions() { return deliveryOptions; } public void setDeliveryOptions(DeliveryOptions config) { deliveryOptions = config; contentMapper.setDeliveryOptions(config); } /** * Add a custom HTML header element. This string will be added into the HTML * HEAD part of the HTML page. This could be CSS, JS or other header elements. * * @param customHeaderContent A custom HEAD element or NULL */ public void setCustomHeaderContent(String customHeaderContent) { contentMapper.setCustomHeaderContent(customHeaderContent); } /** * Change the start page, may be null * * @param currentURI * The currentURI to set * @param forceLoading * true: force rendering of iframe velocity container, will * redraw everything; * false: set new uri without redrawing * iframe. This implies that the content in the iframe has * already been loaded (e.g. by an in line user click) */ private void changeCurrentURI(String uri, boolean forceLoading) { if (uri == null) { uri = ""; myContent.contextPut("isAbsoluteURI", Boolean.FALSE); } else if (uri.startsWith("http")) { // http and https urls are absolute urls that do not need be fetched from // the OLAT server, they retrieve their content from an external content server //TODO:gs a if an absolut uri is loaded the iframe should size to the default site, see functions.js stuff myContent.contextPut("isAbsoluteURI", Boolean.TRUE); } else { // Check for problematic URI that start with '/' (would lead to a logout (login screen)). if (uri.startsWith("/")) uri = uri.substring(1); myContent.contextPut("isAbsoluteURI", Boolean.FALSE); } // set new current uri and push to velocity this.currentUri = uri; myContent.contextPut("currentURI", this.currentUri); if (forceLoading) { // Serve new URI as currentURI, no need to check for inline events contentMapper.setCheckForInlineEvent(false); } else { // Don't redraw iframe myContent.setDirty(false); contentMapper.setCheckForInlineEvent(true); } setPageDownload(isPageDownloadAllowed(currentUri)); } private boolean isPageDownloadAllowed(String uri) { if(!allowDownload || !StringHelper.containsNonWhitespace(uri)) { return false; } // remove any URL parameters String uriLc = uri.toLowerCase(); int qmarkPos = uriLc.indexOf("?"); if (qmarkPos != -1) { // e.g. index.html?olatraw=true uriLc = uriLc.substring(0, qmarkPos); } // remove any anchor references int hTagPos = uri.indexOf("#"); if (hTagPos != -1) { // e.g. index.html#checkThisOut uriLc = uriLc.substring(0, hTagPos); } // HTML pages are rendered inline, everything else is regarded as "downloadable" if(uriLc.endsWith(".html") || uriLc.endsWith(".htm") || uriLc.endsWith(".xhtml")) { return false; } return true; } private void setPageDownload(boolean allow) { if(allow) { if(downloadLink == null) { downloadLink = LinkFactory.createCustomLink(COMMAND_DOWNLOAD, COMMAND_DOWNLOAD, "", Link.NONTRANSLATED, myContent, this); downloadLink.setCustomEnabledLinkCSS("o_download"); downloadLink.setIconLeftCSS("o_icon o_icon_download o_icon-lg"); downloadLink.setTooltip(getTranslator().translate(COMMAND_DOWNLOAD)); } else if (!downloadLink.isVisible()) { downloadLink.setVisible(true); } //update always the name downloadLink.setCustomDisplayText(currentUri); } else { if(downloadLink != null) { downloadLink.setVisible(false); } } } /** * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, * org.olat.core.gui.components.Component, org.olat.core.gui.control.Event) */ @Override public void event(UserRequest ureq, Component source, Event event) { if (source == eventVC) { if (NEW_URI_EVENT.equals(event.getCommand())) { // This event gets triggered from the iframe content by calling a js function outside // Get new uri from JS method and fire to parents String newUri = ureq.getModuleURI(); newUri = ureq.getHttpReq().getParameter("uri"); int baseUriPos = newUri.indexOf(baseURI); if (baseUriPos != -1) { int newUriPos = baseUriPos + baseURI.length(); if (newUri.length() > newUriPos) { newUri = newUri.substring(newUriPos); String hash = ureq.getHttpReq().getParameter("hash"); if (StringHelper.containsNonWhitespace(hash)) { // force iframe reload to fix truncated page problem changeCurrentURI(newUri + '#' + hash, true); fireEvent(ureq, new NewIframeUriEvent(newUri)); } if (newUri.startsWith("/")) { // clean newUri to make equals check work newUri = newUri.substring(1); } if (! newUri.equals(this.currentUri)) { changeCurrentURI(newUri, false); fireEvent(ureq, new NewIframeUriEvent(currentUri)); } // else probably a reload, no need to propagate new uri event } // else ?? don't do anything } // else ?? don't do anything // // don't mark as dirty to prevent re-rendering in AJAX mode - event was a // background event only eventVC.setDirty(false); } } else if (source == downloadLink) { MediaResource mediaResource = contentMapper.deliverFile(ureq.getHttpReq(), currentUri, false); if(mediaResource instanceof VFSMediaResource) { ((VFSMediaResource)mediaResource).setDownloadable(true); } ureq.getDispatchResult().setResultingMediaResource(mediaResource); } } @Override protected void doDispose() { //contentMapper get's unregistered automatically with basic controller // remove us as listener if not already done getWindowControl().getWindowBackOffice().removeCycleListener(this); } /** * this event gets fired from the TextMarkerController when the user swiches on/off textmarking * @see org.olat.core.util.event.GenericEventListener#event(org.olat.core.gui.control.Event) */ @Override public void event(Event event) { if (event instanceof MultiUserEvent) { if (event.getCommand().equals("glossaryOn")) { contentMapper.setEnableTextmarking(true); } else if (event.getCommand().equals("glossaryOff")) { contentMapper.setEnableTextmarking(false); } } else if (event.equals(Window.BEFORE_INLINE_RENDERING)){ // Set the custom CSS URL that is used by the current tab or site if // available. The reason why we do this here and not in the constructor is // that during the constructing phase this property is not yet set on the // window. Window myWindow = getWindowControl().getWindowBackOffice().getWindow(); CustomCSS currentCustomCSS = myWindow.getCustomCSS(); if (currentCustomCSS != null) { contentMapper.setCustomCssDelegate(myWindow); } // done, remove us as listener getWindowControl().getWindowBackOffice().removeCycleListener(this); } } @Override public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { if(entries == null || entries.isEmpty()) return; Long id = entries.get(0).getOLATResourceable().getResourceableId(); if(id == 0) { String path = BusinessControlFactory.getInstance().getPath(entries.get(0)); changeCurrentURI(path, false); } } }