/**
* 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) 1999-2006 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.core.commons.modules.singlepage;
import javax.servlet.http.HttpServletRequest;
import org.olat.core.commons.controllers.linkchooser.CustomLinkTreeModel;
import org.olat.core.commons.editor.htmleditor.WysiwygFactory;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.dispatcher.mapper.MapperRegistry;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.htmlsite.ExternalSiteEvent;
import org.olat.core.gui.components.htmlsite.HtmlStaticPageComponent;
import org.olat.core.gui.components.htmlsite.NewInlineUriEvent;
import org.olat.core.gui.components.htmlsite.OlatCmdEvent;
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.Controller;
import org.olat.core.gui.control.DefaultController;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.clone.CloneableController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.control.generic.iframe.IFrameDisplayController;
import org.olat.core.gui.control.generic.iframe.NewIframeUriEvent;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.NotFoundMediaResource;
import org.olat.core.gui.media.RedirectMediaResource;
import org.olat.core.gui.translator.PackageTranslator;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.context.BusinessControl;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.CoreLoggingResourceable;
import org.olat.core.logging.activity.CourseLoggingAction;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.Formatter;
import org.olat.core.util.Util;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSMediaResource;
/**
* Description:<BR>
* Wrapper controller that shows local html pages from the given folder / filename
* <P/>
* Initial Date: Dec 16, 2004
*
* EVENTS: to listening controllers:
* - OlatCmdEvent (which has to be accepted by calling accept() on the event)
* - NewInlineUriEvent if the user changed the page by clicking on a link
*
* @author gnaegi
*/
public class SinglePageController extends DefaultController implements CloneableController {
private static final String PACKAGE = Util.getPackageName(SinglePageController.class);
private static final String VELOCITY_ROOT = Util.getPackageVelocityRoot(SinglePageController.class);
private static final String GOTO_NID = "GOTO_NID: ";
private static final String COMMAND_EDIT = "command.edit";
private HtmlStaticPageComponent cpc;
private VelocityContainer myContent;
private PackageTranslator trans;
// mapper for the external site
private Mapper mapper;
private MapperRegistry mr;
private String amapPath;
private IFrameDisplayController idc;
private String g_curURI;
// save constructor args to remember if we open a site in a new window
private String g_fileName;
private boolean g_inIframe;
private boolean g_allowRelativeLinks;
private VFSContainer g_rootContainer;
private VFSContainer g_new_rootContainer;
private String g_initialUri;
private boolean g_showHomeLink;
private Controller htmlEditorController;
private Link editLink;
private CustomLinkTreeModel customLinkTreeModel;
private CloseableModalController cmc;
private boolean wasStartpage = true;
private VelocityContainer homeLinkContent;
private Link homeLink;
private Panel homeLinkP;
/**
* Constructor for the generic single page controller.
*
* displays the html page (or any file if in iframe mode) and, if not on the first page and not in iframe mode,
* offers a button to return to the start page.
* (useful for a "home" button)
* <p>
* You can call the allowPageEditing after this construtor to allow users to edit the page
*
* @param folderPath The course folder which contains the single page html file
* @param inIframe if true, the contents are rendered within an iframe
* @param fileName the relative filePath in the material folder starting with a slash, e.g. /welcome.html or /docu/info.html
* @param rootContainer the root from which to resolve the files (like "the htdocs directory")
* @param currentUri if not null, the start page is set to this uri (instead of the fileName arg). relative to the -corrected- rootcontainer if !allowRelativeLinks
*
* @param allowRelativeLinks if true, an initial uri of /folder/a.html allows navigating till "/", if false, only
* navigating in /folder/ and subfolders of this folder is allowed
*
*
*/
public SinglePageController(UserRequest ureq, WindowControl wControl, boolean inIframe, final VFSContainer rootContainer, final String fileName, String currentUri, boolean allowRelativeLinks, OLATResourceable ores) {
//default behavior is to show the home link in a single page
this(ureq, wControl, inIframe, rootContainer, fileName, currentUri, allowRelativeLinks, true, ores);
}
/**
*
* @param ureq
* @param wControl
* @param inIframe
* @param rootContainer
* @param fileName
* @param currentUri
* @param allowRelativeLinks
* @param showHomeLink
*/
public SinglePageController(UserRequest ureq, WindowControl wControl, boolean inIframe, final VFSContainer rootContainer, final String fileName, String currentUri, boolean allowRelativeLinks, boolean showHomeLink) {
//default behavior is to show the home link in a single page
this(ureq, wControl, inIframe, rootContainer, fileName, currentUri, allowRelativeLinks, showHomeLink, null);
}
/**
* Constructor for the generic single page controller.
*
* displays the html page (or any file if in iframe mode) and, if not on the first page and not in iframe mode,
* offers a button to return to the start page.
* (useful for a "home" button)
* <p>
* You can call the allowPageEditing after this construtor to allow users to edit the page
*
* @param folderPath The course folder which contains the single page html file
* @param inIframe if true, the contents are rendered within an iframe
* @param fileName the relative filePath in the material folder starting with a slash, e.g. /welcome.html or /docu/info.html
* @param rootContainer the root from which to resolve the files (like "the htdocs directory")
* @param currentUri if not null, the start page is set to this uri (instead of the fileName arg). relative to the -corrected- rootcontainer if !allowRelativeLinks
*
* @param allowRelativeLinks if true, an initial uri of /folder/a.html allows navigating till "/", if false, only
* navigating in /folder/ and subfolders of this folder is allowed
* @param showHomeLink true enables the home link icon and link which is added to the SP, false removes icon and link.
*
*
*/
public SinglePageController(UserRequest ureq, WindowControl wControl, boolean inIframe, VFSContainer rootContainer, String fileName, String currentUri, boolean allowRelativeLinks, boolean showHomeLink, OLATResourceable contextResourcable) {
super(wControl);
trans = new PackageTranslator(PACKAGE, ureq.getLocale());
Panel mainP = new Panel("iframemain");
myContent = new VelocityContainer("singlepagecontent", VELOCITY_ROOT + "/index.html", trans, this);
homeLinkP = new Panel("homelink");
homeLinkContent = new VelocityContainer("homelinkcontent", VELOCITY_ROOT + "/homelink.html", trans, this);
homeLinkContent.contextPut("showHomeLink",showHomeLink?Boolean.TRUE:Boolean.FALSE);
homeLink = LinkFactory.createCustomLink("command.home", "command.home", "", Link.NONTRANSLATED, homeLinkContent, this);
homeLink.setCustomEnabledLinkCSS("b_content_reset");
homeLink.setTooltip(trans.translate("command.home"), false);
myContent.put("homelinkpanel", homeLinkP);
// remember values in case of later cloning
// g_fileName : initial file name given (no root correction), e.g. bla.html or f/g/blu.html
// always use non-iframe mode for screenreaders
this.g_inIframe = (inIframe && (! getWindowControl().getWindowBackOffice().getWindowManager().isForScreenReader()));
this.g_showHomeLink = showHomeLink;
this.g_allowRelativeLinks = allowRelativeLinks;
this.g_fileName = fileName;
this.g_rootContainer = rootContainer;
boolean jumpIn = false;
// strip beginning slash
String startURI = ( (fileName.charAt(0) == '/')? fileName.substring(1) : fileName);
// jump (e.g. from search) to the path if the business-launch-path says so.
BusinessControl bc = getWindowControl().getBusinessControl();
ContextEntry ce = bc.popLauncherContextEntry();
if ( ce != null ) { // a context path is left for me
Tracing.logDebug("businesscontrol (for further jumps) would be:"+bc, SinglePageController.class);
OLATResourceable ores = ce.getOLATResourceable();
Tracing.logDebug("OLATResourceable=" + ores, SinglePageController.class);
String typeName = ores.getResourceableTypeName();
// typeName format: 'path=/test1/test2/readme.txt'
// First remove prefix 'path='
String path = typeName.substring("path=".length());
if (path.length() > 0) {
Tracing.logDebug("direct navigation to container-path=" + path, SinglePageController.class);
jumpIn = true;
currentUri = path;
startURI = path;
}
}
// adjust root folder if security does not allow using ../.. etc.
if (!allowRelativeLinks && !jumpIn) {
// start uri is filename without relative path.
// the relative path of the file is added to the vfs rootcontainer
int sla = startURI.lastIndexOf('/');
if (sla != -1) {
String root = startURI.substring(0,sla);
startURI = startURI.substring(sla+1);
VFSContainer newroot = (VFSContainer)rootContainer.resolve(root);
this.g_new_rootContainer = newroot;
} else {
this.g_new_rootContainer = rootContainer;
}
} else {
this.g_new_rootContainer = rootContainer;
}
this.g_initialUri = startURI;
setCurURI(startURI);
// startURI and g_new_rootContainer set
// g_curURI : the current uri (relative to the (evt. corrected) rootcontainer)
// g_new_rootContainer : the given rootcontainer or adjusted in case when relativelinks are not allowed
// Display in iframe when
// a) configured as to be displayed in iframe and not in braille mode
// b) page is a direct jump in (unclear why not in this case, code was like that)
// c) when page type can not be inline rendered (e.g. when page is a pdf file)
if (g_inIframe || jumpIn || !HtmlStaticPageComponent.isFileTypeSupported(startURI)) {
idc = new IFrameDisplayController(ureq, getWindowControl(), g_new_rootContainer, contextResourcable);
idc.addControllerListener(this);
idc.setCurrentURI(startURI);
myContent.put("content", idc.getInitialComponent());
} else {
// in inline mode
// create single page root file now and start component for display dispathing
cpc = new HtmlStaticPageComponent("content", g_new_rootContainer);
cpc.addListener(this);
myContent.put("content", cpc);
if (currentUri != null) {
if (currentUri.charAt(0) == '/') {
//strip beginning slash
currentUri = currentUri.substring(1);
}
setCurURI(currentUri);
cpc.setCurrentURI(currentUri);
} else {
// no bookmarked uri given
setCurURI(startURI);
cpc.setCurrentURI(startURI);
}
}
mainP.setContent(myContent);
setInitialComponent(mainP);
}
/**
* When you call this method the edit mode will be enabled. By default no edit
* is possible, you have to call this method after constrution time explicitly
*/
public void allowPageEditing() {
editLink = LinkFactory.createCustomLink(COMMAND_EDIT, COMMAND_EDIT, "", Link.NONTRANSLATED, myContent, this);
editLink.setCustomEnabledLinkCSS("b_content_edit");
editLink.setTooltip(trans.translate(COMMAND_EDIT), false);
}
public void setAllowDownload(boolean allow) {
if (idc != null) {
// can be null because the boolean "inIframe" in the constructor does not
// always use the iframe. When in braille mode the system renders the page inline in any case.
idc.setAllowDownload(allow);
}
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
*/
public void event(UserRequest ureq, Controller source, Event event) {
if (source == idc) {
if (event instanceof OlatCmdEvent) {
//TODO:gs legacy code???
//FIXME:fj:b move to other place (whole class) since single page controller could be used generically
OlatCmdEvent oce = (OlatCmdEvent) event;
String nodeId = oce.getSubcommand();
ThreadLocalUserActivityLogger.log(CourseLoggingAction.COURSE_BROWSE_GOTO_NODE, getClass(),
CoreLoggingResourceable.wrapSpUri(GOTO_NID+nodeId));
// refire to listening controllers
fireEvent(ureq, event);
} else if (event instanceof NewIframeUriEvent) {
NewIframeUriEvent iframeEvent = (NewIframeUriEvent) event;
String newUri = iframeEvent.getNewUri();
setCurURI(newUri);
// log this uri change
ThreadLocalUserActivityLogger.log(CourseLoggingAction.NODE_SINGLEPAGE_GET_FILE, getClass(),
CoreLoggingResourceable.wrapSpUri(newUri));
}
} else if (source == htmlEditorController) {
htmlEditorController.dispose();
cmc.deactivate();
if (g_inIframe) {
idc.setCurrentURI(g_curURI);
} else {
cpc.setCurrentURI(g_curURI);
}
} else if (source == cpc) {
if (event instanceof OlatCmdEvent) {
OlatCmdEvent oce = (OlatCmdEvent) event;
String nodeId = oce.getSubcommand();
ThreadLocalUserActivityLogger.log(CourseLoggingAction.COURSE_BROWSE_GOTO_NODE, getClass(),
CoreLoggingResourceable.wrapSpUri(GOTO_NID+nodeId));
// refire to listening controllers
fireEvent(ureq, event);
}
else if (event instanceof NewInlineUriEvent) {
// adapt path if needed and refire to listening controllers
String opath = ((NewInlineUriEvent)event).getNewUri();
setCurURI(opath);
fireEvent(ureq, event);
NewInlineUriEvent iue = (NewInlineUriEvent) event;
String newUri = iue.getNewUri();
ThreadLocalUserActivityLogger.log(CourseLoggingAction.NODE_SINGLEPAGE_GET_FILE, getClass(),
CoreLoggingResourceable.wrapSpUri(newUri));
}
else if (event instanceof ExternalSiteEvent) {
ExternalSiteEvent ese = (ExternalSiteEvent)event;
String startUri = ese.getStartUri();
final VFSContainer finalRootContainer = g_new_rootContainer;
if (mapper == null) {
mr = MapperRegistry.getInstanceFor(ureq.getUserSession());
mapper = createMapper(finalRootContainer);
amapPath = mr.register(mapper);
}
ese.setResultingMediaResource(new RedirectMediaResource(amapPath+"/"+startUri));
ese.accept();
}
}
}
/**
* @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)
*/
public void event(UserRequest ureq, Component source, Event event) {
if (source == homeLink) {
// go home
setCurURI(g_initialUri);
if (g_inIframe) {
idc.setCurrentURI(g_curURI);
} else {
cpc.setCurrentURI(g_curURI);
fireEvent(ureq, new NewInlineUriEvent(g_initialUri));
}
} else if (source == editLink) {
if (event.getCommand().equals(COMMAND_EDIT)) {
if (g_curURI == null || g_new_rootContainer.resolve(g_curURI) == null) {
getWindowControl().setError(trans.translate("error.pagenotfound"));
return;
}
if (htmlEditorController != null) htmlEditorController.dispose();
if (customLinkTreeModel == null) {
htmlEditorController = WysiwygFactory.createWysiwygController(ureq, getWindowControl(), g_new_rootContainer, g_curURI, true);
} else {
htmlEditorController = WysiwygFactory.createWysiwygControllerWithInternalLink(ureq, getWindowControl(), g_new_rootContainer,
g_curURI, true, customLinkTreeModel);
}
htmlEditorController.addControllerListener(this);
cmc = new CloseableModalController(getWindowControl(), trans.translate("close"), htmlEditorController.getInitialComponent());
cmc.activate();
}
}
}
private void setCurURI(String uri) {
this.g_curURI = uri;
// update start button visibility
boolean isStartpage = g_initialUri.equals(uri);
if (isStartpage != wasStartpage) {
//detect change, to avoid unecessary iFrame reloads
if (isStartpage) {
homeLinkP.setContent(null);
} else {
if (g_showHomeLink) homeLinkP.setContent(homeLinkContent);
}
wasStartpage = isStartpage;
}
}
@SuppressWarnings("unused")
private Mapper createMapper(final VFSContainer rootContainer) {
Mapper map = new Mapper() {
public MediaResource handle(String relPath,HttpServletRequest request) {
VFSItem currentItem = rootContainer.resolve(relPath);
if (currentItem == null || (currentItem instanceof VFSContainer)) {
return new NotFoundMediaResource(relPath);
}
return new VFSMediaResource((VFSLeaf)currentItem);
}
};
return map;
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
*/
protected void doDispose() {
// NOTE: do not deregister this mapper here: the url pointing to this mapper is opened in a new browser window
// and the user will expect to be able to browse beyond the lifetime of this originating controller here.
//if (mapper != null) {mr.deregister(mapper);}
if (idc != null) {
idc.dispose();
idc = null;
}
if (htmlEditorController != null) {
htmlEditorController.dispose();
htmlEditorController = null;
}
}
/**
* Set the internal link tree model that should be used in the HTML editor to
* display links
*
* @param customLinkTreeModel
*/
public void setInternalLinkTreeModel(CustomLinkTreeModel customLinkTreeModel) {
this.customLinkTreeModel = customLinkTreeModel;
}
/**
* @see org.olat.core.gui.control.generic.clone.CloneableController#cloneController(org.olat.core.gui.UserRequest, org.olat.core.gui.control.WindowControl)
*/
public Controller cloneController(UserRequest ureq, WindowControl control) {
return new SinglePageController(ureq, control, g_inIframe, g_rootContainer, g_fileName, g_curURI, g_allowRelativeLinks, g_showHomeLink, null);
}
/**
* Set a scale factor to enlarge / shrink the entire page. This is handy when
* a preview of a page should be displayed.
* E.g: 2 will scale the page by factor 2, 0.5 will shrink the page by factor 2
*
* @param scaleFactor > 0 or 1: don't scale; < 1: shrink page; > 1 enlarge page.
* @param displayHeight > 0: size to fit; > 0: fixed size in pixel
* @param hideOverflow true: don't show scroll-bars; false: default behavior with scroll-bars
*/
public void setScaleFactorAndHeight(float scaleFactor, int displayHeight, boolean hideOverflow) {
String cssRule = "";
// add rule for scaling
if (scaleFactor > 0 && scaleFactor != 1) {
String formattedScaleFactor = Formatter.roundToString(scaleFactor, 2);
cssRule = "zoom: " + formattedScaleFactor + "; -moz-transform: scale(" + formattedScaleFactor + ");";
}
// add rule to set fix height
if (displayHeight > 0) {
if (idc != null) {
idc.setHeightPX(displayHeight);
} else {
// add to rule for html component, so such feature available
cssRule += "height: " + displayHeight + "px;";
}
}
// add overflow rule
if (hideOverflow) {
cssRule += "overflow: hidden;";
}
// cleanup
if (cssRule.length() == 0) {
cssRule = null;
}
// apply css rule to iframe controller or html component
if (idc != null) {
if (cssRule == null) {
idc.setCustomHeaderContent(null);
} else {
idc.setCustomHeaderContent("<style type='text/css'>body {" + cssRule + "}</style>");
}
} else {
cpc.setWrapperCssStyle(cssRule);
}
}
}