/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package com.celements.web;
import java.io.IOException;
import java.util.Date;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.celements.filebase.IAttachmentServiceRole;
import com.celements.filebase.matcher.AttNameWithoutExtMatcher;
import com.celements.filebase.matcher.IAttFileNameMatcherRole;
import com.celements.model.access.exception.AttachmentNotExistsException;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.util.Util;
import com.xpn.xwiki.web.SkinAction;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.web.XWikiResponse;
/**
* <p>
* Action for serving skin files. It allows skins to be defined using XDocuments as skins,
* by letting files be placed as text fields in an XWiki.XWikiSkins object, or as
* attachments to the document, or as a file in the filesystem. If the file is not found
* in the current skin, then it is searched in its base skin, and eventually in the
* default base skins,
* </p>
* <p>
* This action indicates that the results should be publicly cacheable for 30 days.
* </p>
*
* @version $Id: FileAction.java 28858 2010-05-13 20:43:41Z sdumitriu $
* @since 1.0
*/
public class FileAction extends SkinAction {
/** Logging helper. */
private static final Logger _Logger = LoggerFactory.getLogger(FileAction.class);
/** Path delimiter. */
public static final String DELIMITER = "/";
/** The directory where the skins are placed in the webapp. */
private static final String SKINS_DIRECTORY = "skins";
/** The directory where resources are placed in the webapp. */
public static final String RESOURCES_DIRECTORY = "resources";
/**
* The encoding to use when reading text resources from the filesystem and when sending
* css/javascript responses.
*/
private static final String ENCODING = "UTF-8";
@Override
public String render(String path, XWikiContext context) throws XWikiException, IOException {
XWiki xwiki = context.getWiki();
// Since skin paths usually contain the name of skin document, it is likely
// that the context document belongs to
// the current skin.
XWikiDocument doc = context.getDoc();
// The base skin could be either a filesystem directory, or an xdocument.
String baseskin = xwiki.getBaseSkin(context, true);
XWikiDocument baseskindoc = xwiki.getDocument(baseskin, context);
// The default base skin is always a filesystem directory.
String defaultbaseskin = xwiki.getDefaultBaseSkin(context);
if (_Logger.isDebugEnabled()) {
_Logger.debug("document: " + doc.getFullName() + " ; baseskin: " + baseskin
+ " ; defaultbaseskin: " + defaultbaseskin);
}
// Since we don't know exactly what does the URL point at, meaning that we
// don't know where the skin identifier
// ends and where the path to the file starts, we must try to split at every
// '/' character.
int idx = path.lastIndexOf(DELIMITER);
boolean found = false;
while (idx > 0) {
try {
String filename = Util.decodeURI(path.substring(idx + 1), context);
if (_Logger.isDebugEnabled()) {
_Logger.debug("Trying '" + filename + "'");
}
// Try on the current skin document.
if (renderSkin(filename, doc, context)) {
found = true;
break;
}
// Try on the base skin document, if it is not the same as above.
if (!doc.getName().equals(baseskin)) {
if (renderSkin(filename, baseskindoc, context)) {
found = true;
break;
}
}
// Try on the default base skin, if it wasn't already tested above.
if (!(doc.getName().equals(defaultbaseskin) || baseskin.equals(defaultbaseskin))) {
// defaultbaseskin can only be on the filesystem, so don't try to use
// it as a
// skin document.
if (renderFileFromFilesystem(getSkinFilePath(filename, defaultbaseskin), context)) {
found = true;
break;
}
}
// Try in the resources directory.
if (renderFileFromFilesystem(getResourceFilePath(filename), context)) {
found = true;
break;
}
} catch (XWikiException ex) {
if (ex.getCode() == XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION) {
// This means that the response couldn't be sent, although the file
// was
// successfully found. Signal this further, and stop trying to render.
throw ex;
}
_Logger.debug(Integer.toString(idx), ex);
}
idx = path.lastIndexOf(DELIMITER, idx - 1);
}
if (!found) {
context.getResponse().setStatus(404);
return "docdoesnotexist";
}
return null;
}
/**
* Tries to serve a skin file using <tt>doc</tt> as a skin document. The file is
* searched in the following places:
* <ol>
* <li>As the content of a property with the same name as the requested filename, from
* an XWikiSkins object attached to the document.</li>
* <li>As the content of an attachment with the same name as the requested filename.
* </li>
* <li>As a file located on the filesystem, in the directory with the same name as the
* current document (in case the URL was actually pointing to
* <tt>/skins/directory/file</tt>).</li>
* </ol>
*
* @param filename
* The name of the skin file that should be rendered.
* @param doc
* The skin {@link XWikiDocument document}.
* @param context
* The current {@link XWikiContext request context}.
* @return <tt>true</tt> if the attachment was found and the content was successfully
* sent.
* @throws XWikiException
* If the attachment cannot be loaded.
* @throws IOException
* if the filename is invalid
*/
private boolean renderSkin(String filename, XWikiDocument doc, XWikiContext context)
throws XWikiException, IOException {
if (_Logger.isDebugEnabled()) {
_Logger.debug("Rendering file '" + filename + "' within the '" + doc.getFullName()
+ "' document");
}
try {
if (doc.isNew()) {
if (_Logger.isDebugEnabled()) {
_Logger.debug(doc.getName() + " is not a document");
}
} else {
return renderFileFromObjectField(filename, doc, context) || renderFileFromAttachment(
filename, doc, context) || (SKINS_DIRECTORY.equals(doc.getSpace())
&& renderFileFromFilesystem(getSkinFilePath(filename, doc.getName()), context));
}
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION,
"Exception while sending response:", e);
}
return renderFileFromFilesystem(getSkinFilePath(filename, doc.getName()), context);
}
/**
* Tries to serve a file from the filesystem.
*
* @param path
* Path of the file that should be rendered.
* @param context
* The current {@link XWikiContext request context}.
* @return <tt>true</tt> if the file was found and its content was successfully sent.
* @throws XWikiException
* If the response cannot be sent.
*/
private boolean renderFileFromFilesystem(String path, XWikiContext context)
throws XWikiException {
if (_Logger.isDebugEnabled()) {
_Logger.debug("Rendering filesystem file from path [" + path + "]");
}
XWikiResponse response = context.getResponse();
try {
byte[] data = context.getWiki().getResourceContentAsBytes(path);
if (data != null && data.length > 0) {
String filename = path.substring(path.lastIndexOf("/") + 1, path.length());
String mimetype = context.getEngineContext().getMimeType(filename.toLowerCase());
Date modified = context.getWiki().getResourceLastModificationDate(path);
if (isCssMimeType(mimetype) || isJavascriptMimeType(mimetype)) {
response.setCharacterEncoding(ENCODING);
}
setupHeaders(response, mimetype, modified, data.length);
try {
response.getOutputStream().write(data);
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION,
"Exception while sending response", e);
}
return true;
}
} catch (IOException ex) {
_Logger.info("Skin file '" + path + "' does not exist or cannot be accessed");
}
return false;
}
/**
* Tries to serve the content of an XWikiSkins object field as a skin file.
*
* @param filename
* The name of the skin file that should be rendered.
* @param doc
* The skin {@link XWikiDocument document}.
* @param context
* The current {@link XWikiContext request context}.
* @return <tt>true</tt> if the object exists, and the field is set to a non-empty
* value, and its content was successfully sent.
* @throws IOException
* If the response cannot be sent.
*/
@Override
public boolean renderFileFromObjectField(String filename, XWikiDocument doc, XWikiContext context)
throws IOException {
_Logger.debug("... as object property");
BaseObject object = doc.getObject("XWiki.XWikiSkins");
String content = null;
if (object != null) {
content = object.getStringValue(filename);
}
if (!StringUtils.isBlank(content)) {
XWiki xwiki = context.getWiki();
XWikiResponse response = context.getResponse();
String mimetype = xwiki.getEngineContext().getMimeType(filename.toLowerCase());
// Since object fields are read as unicode strings, the result does not
// depend on the wiki encoding. Force
// the output to UTF-8.
response.setCharacterEncoding(ENCODING);
byte[] data = content.getBytes(ENCODING);
setupHeaders(response, mimetype, doc.getDate(), data.length);
response.getOutputStream().write(data);
return true;
} else {
_Logger.debug("Object field not found or empty");
}
return false;
}
/**
* Tries to serve the content of an attachment as a skin file.
*
* @param filename
* The name of the skin file that should be rendered.
* @param doc
* The skin {@link XWikiDocument document}.
* @param context
* The current {@link XWikiContext request context}.
* @return <tt>true</tt> if the attachment was found and its content was successfully
* sent.
* @throws IOException
* If the response cannot be sent.
* @throws XWikiException
* If the attachment cannot be loaded.
*/
@Override
public boolean renderFileFromAttachment(String filename, XWikiDocument doc, XWikiContext context)
throws IOException, XWikiException {
_Logger.debug("... as attachment");
try {
IAttFileNameMatcherRole matcher = Utils.getComponent(IAttFileNameMatcherRole.class,
AttNameWithoutExtMatcher.ATT_NAME_WITHOUT_EXT_MATCHER);
matcher.setFileNamePattern(filename);
XWikiAttachment attachment = getAttachmentService().getAttachmentFirstNameMatch(doc, matcher);
XWiki xwiki = context.getWiki();
XWikiResponse response = context.getResponse();
String mimetype = xwiki.getEngineContext().getMimeType(filename.toLowerCase());
if (isCssMimeType(mimetype) || isJavascriptMimeType(mimetype)) {
byte[] data = attachment.getContent(context);
// Always force UTF-8, as this is the assumed encoding for text files.
response.setCharacterEncoding(ENCODING);
setupHeaders(response, mimetype, attachment.getDate(), data.length);
response.getOutputStream().write(data);
} else {
setupHeaders(response, mimetype, attachment.getDate(), attachment.getContentSize(context));
IOUtils.copy(attachment.getContentInputStream(context), response.getOutputStream());
}
return true;
} catch (AttachmentNotExistsException anee) {
_Logger.debug("Attachment not found", anee);
}
return false;
}
public IAttachmentServiceRole getAttachmentService() {
return Utils.getComponent(IAttachmentServiceRole.class);
}
}