/*
* 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.xpn.xwiki.web;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.csrf.CSRFToken;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
/**
* Action used for saving and returning to the edit page rather than viewing changes.
*
* @version $Id: 6faffaa6bc4bc22c7048498cddc8aed2b4c861b1 $
*/
public class SaveAndContinueAction extends XWikiAction
{
/** Key for storing the wrapped action in the context. */
private static final String WRAPPED_ACTION_CONTEXT_KEY = "SaveAndContinueAction.wrappedAction";
/** Logger. */
private static final Logger LOGGER = LoggerFactory.getLogger(SaveAndContinueAction.class);
/**
* Write an error response to an ajax request.
*
* @param httpStatusCode The status code to set on the response.
* @param message The message that should be displayed.
* @param context the context.
*/
private void writeAjaxErrorResponse(int httpStatusCode, String message, XWikiContext context)
{
try {
context.getResponse().setContentType("text/plain");
context.getResponse().setStatus(httpStatusCode);
context.getResponse().setCharacterEncoding(context.getWiki().getEncoding());
context.getResponse().getWriter().print(message);
} catch (IOException e) {
LOGGER.error("Failed to send error response to AJAX save and continue request.", e);
}
}
/**
* Perform the internal action implied by the save and continue request. If the request is an ajax request,
* writeAjaxErrorResponse will be called. The return value will be that of the wrapped action.
*
* @param isAjaxRequest Indicate if this is an ajax request.
* @param back The back URL.
* @param context The xwiki context.
* @return {\code false} if the request is an ajax request, otherwise the return value of the wrapped action.
* @throws XWikiException
*/
private boolean doWrappedAction(boolean isAjaxRequest, String back, XWikiContext context) throws XWikiException
{
boolean failure = false;
// This will never be true if "back" comes from request.getHeader("referer")
if (back != null && back.contains("editor=class")) {
PropUpdateAction pua = new PropUpdateAction();
if (pua.propUpdate(context)) {
if (isAjaxRequest) {
String errorMessage = localizePlainOrKey((String) context.get("message"));
writeAjaxErrorResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMessage, context);
} else {
context.put(WRAPPED_ACTION_CONTEXT_KEY, pua);
}
failure = true;
}
} else {
SaveAction sa = new SaveAction();
if (sa.save(context)) {
if (isAjaxRequest) {
String errorMessage =
localizePlainOrKey("core.editors.saveandcontinue.theDocumentWasNotSaved");
// This should not happen. SaveAction.save(context) should normally throw an
// exception when failing during save and continue.
LOGGER.error("SaveAction.save(context) returned true while using save & continue");
writeAjaxErrorResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMessage, context);
} else {
context.put(WRAPPED_ACTION_CONTEXT_KEY, sa);
}
failure = true;
} else {
// Lock back the document
context.getDoc().getTranslatedDocument(context).setLock(context.getUser(), context);
}
}
return failure;
}
/**
* @param isAjaxRequest Indicate if this is an ajax request.
* @param context The XWiki context.
* @throws XWikiException unless it is an ajax request.
*/
private void handleCSRFValidationFailure(boolean isAjaxRequest, XWikiContext context)
throws XWikiException
{
final String csrfCheckFailedMessage = localizePlainOrKey("core.editors.saveandcontinue.csrfCheckFailed");
if (isAjaxRequest) {
writeAjaxErrorResponse(HttpServletResponse.SC_FORBIDDEN,
csrfCheckFailedMessage,
context);
} else {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_ACCESS_TOKEN_INVALID,
csrfCheckFailedMessage);
}
}
/**
* @param isAjaxRequest Indicate if this is an ajax request.
* @param exception The exception to handle.
* @param context The XWiki context.
* @throws XWikiException unless it is an ajax request.
*/
private void handleException(boolean isAjaxRequest, Exception exception, XWikiContext context)
throws XWikiException
{
if (isAjaxRequest) {
String errorMessage =
localizePlainOrKey("core.editors.saveandcontinue.exceptionWhileSaving", exception.getMessage());
writeAjaxErrorResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMessage, context);
String logMessage = "Caught exception during save and continue";
if (exception instanceof XWikiException) {
LOGGER.info(logMessage, exception);
} else {
LOGGER.error(logMessage, exception);
}
} else {
if (exception instanceof XWikiException) {
throw (XWikiException) exception;
} else {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_UNKNOWN,
"Uncaught exception", exception);
}
}
}
@Override
public boolean action(XWikiContext context) throws XWikiException
{
CSRFToken csrf = Utils.getComponent(CSRFToken.class);
String token = context.getRequest().getParameter("form_token");
// If the request is an ajax request, we will:
//
// 1) _not_ send a redirect response
//
// 2) if for any reason the document is not saved, call the method writeAjaxErrorResponse and return false
// (which normally indicates success).
final boolean isAjaxRequest = Utils.isAjaxRequest(context);
if (!csrf.isTokenValid(token)) {
handleCSRFValidationFailure(isAjaxRequest, context);
return false;
}
// Try to find the URL of the edit page which we came from
String back = findBackURL(context);
try {
if (doWrappedAction(isAjaxRequest, back, context)) {
return !isAjaxRequest;
}
} catch (Exception e) {
handleException(isAjaxRequest, e, context);
return !isAjaxRequest;
}
// If this is an ajax request, no need to redirect.
if (isAjaxRequest) {
context.getResponse().setStatus(HttpServletResponse.SC_NO_CONTENT);
return false;
}
// Forward back to the originating page
try {
context.getResponse().sendRedirect(back);
} catch (IOException ignored) {
// This exception is ignored because it will only be thrown if content has already been sent to the
// response. This should never happen but we have to catch the exception anyway.
}
return false;
}
@Override
public String render(XWikiContext context) throws XWikiException
{
XWikiAction wrappedAction = (XWikiAction) context.get(WRAPPED_ACTION_CONTEXT_KEY);
if (wrappedAction != null) {
return wrappedAction.render(context);
}
return "exception";
}
/**
* Try to find the URL of the edit page which we came from.
*
* @param context current xwiki context
* @return URL of the edit page
*/
private String findBackURL(XWikiContext context)
{
XWikiRequest request = context.getRequest();
String back = request.getParameter("xcontinue");
if (StringUtils.isEmpty(back)) {
back = request.getParameter("xredirect");
}
if (StringUtils.isEmpty(back)) {
back = removeAllParametersFromQueryStringExceptEditor(request.getHeader("Referer"));
}
if (StringUtils.isEmpty(back)) {
back = context.getDoc().getURL("edit", context);
}
return back;
}
/**
* @param url the URL to get a modified version of.
* @return A modified version of the input url where all parameters are stripped from the query string except
* "editor"
*/
private String removeAllParametersFromQueryStringExceptEditor(String url)
{
if (url == null) {
return "";
}
String[] baseAndQuery = url.split("\\?");
// No query string: no change.
if (baseAndQuery.length < 2) {
return url;
}
String[] queryBeforeAndAfterEditor = baseAndQuery[1].split("editor=");
// No editor=* in query string: return URI
if (queryBeforeAndAfterEditor.length < 2) {
return baseAndQuery[0];
}
return baseAndQuery[0] + "?editor=" + queryBeforeAndAfterEditor[1].split("&")[0];
}
}