All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.structr.web.servlet.HtmlServlet Maven / Gradle / Ivy

Go to download

Structr is an open source framework based on the popular Neo4j graph database.

The newest version!
/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr .
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Structr.  If not, see .
 */
package org.structr.web.servlet;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.structr.common.AccessMode;
import org.structr.common.GraphObjectComparator;
import org.structr.common.PathHelper;
import org.structr.common.SecurityContext;
import org.structr.common.ThreadLocalMatcher;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.Result;
import org.structr.core.Services;
import org.structr.core.app.App;
import org.structr.core.app.Query;
import org.structr.core.app.StructrApp;
import org.structr.core.auth.Authenticator;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.Principal;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyKey;
import org.structr.dynamic.File;
import org.structr.rest.auth.AuthHelper;
import org.structr.rest.service.HttpService;
import org.structr.rest.service.HttpServiceServlet;
import org.structr.rest.service.StructrHttpServiceConfig;
import org.structr.schema.ConfigurationProvider;
import org.structr.util.Base64;
import org.structr.web.auth.UiAuthenticator;
import org.structr.web.common.FileHelper;
import org.structr.web.common.RenderContext;
import org.structr.web.common.RenderContext.EditMode;
import org.structr.web.common.StringRenderBuffer;
import org.structr.web.entity.Linkable;
import org.structr.web.entity.Site;
import org.structr.web.entity.User;
import org.structr.web.entity.dom.DOMNode;
import org.structr.web.entity.dom.Page;

//~--- classes ----------------------------------------------------------------
/**
 * Main servlet for content rendering.
 *
 *
 *
 */
public class HtmlServlet extends HttpServlet implements HttpServiceServlet {

	private static final Logger logger = Logger.getLogger(HtmlServlet.class.getName());

	public static final String CONFIRM_REGISTRATION_PAGE = "/confirm_registration";
	public static final String RESET_PASSWORD_PAGE       = "/reset-password";
	public static final String POSSIBLE_ENTRY_POINTS_KEY = "possibleEntryPoints";
	public static final String DOWNLOAD_AS_FILENAME_KEY  = "filename";
	public static final String RANGE_KEY                 = "range";
	public static final String DOWNLOAD_AS_DATA_URL_KEY  = "as-data-url";
	public static final String CONFIRM_KEY_KEY           = "key";
	public static final String TARGET_PAGE_KEY           = "target";
	public static final String ERROR_PAGE_KEY            = "onerror";

	public static final String CUSTOM_RESPONSE_HEADERS      = "HtmlServlet.customResponseHeaders";
	public static final String OBJECT_RESOLUTION_PROPERTIES = "HtmlServlet.resolveProperties";

	private static final String defaultCustomResponseHeaders = "Strict-Transport-Security:max-age=60,"
				+ "X-Content-Type-Options:nosniff,"
				+ "X-Frame-Options:SAMEORIGIN,"
				+ "X-XSS-Protection:1;mode=block";
	private static List customResponseHeaders = Collections.EMPTY_LIST;

	private static final ThreadLocalMatcher threadLocalUUIDMatcher = new ThreadLocalMatcher("[a-fA-F0-9]{32}");
	private static final ExecutorService threadPool = Executors.newCachedThreadPool();

	private final StructrHttpServiceConfig config = new StructrHttpServiceConfig();
	private final Set possiblePropertyNamesForEntityResolving   = new LinkedHashSet<>();

	private boolean isAsync = false;


	@Override
	public StructrHttpServiceConfig getConfig() {
		return config;
	}

	public HtmlServlet() {

		String customResponseHeadersString = Services.getBaseConfiguration().getProperty(CUSTOM_RESPONSE_HEADERS);

		if (StringUtils.isBlank(customResponseHeadersString)) {

			customResponseHeadersString = defaultCustomResponseHeaders;
		}

		if (StringUtils.isNotBlank(customResponseHeadersString)) {
			customResponseHeaders = Arrays.asList(customResponseHeadersString.split("[ ,]+"));
		}

		// resolving properties
		final String resolvePropertiesSource = StructrApp.getConfigurationValue(OBJECT_RESOLUTION_PROPERTIES, "AbstractNode.name");
		for (final String src : resolvePropertiesSource.split("[, ]+")) {

			final String name = src.trim();
			if (StringUtils.isNotBlank(name)) {

				possiblePropertyNamesForEntityResolving.add(name);
			}
		}

		this.isAsync = Services.parseBoolean(Services.getBaseConfiguration().getProperty(HttpService.ASYNC), true);
	}

	@Override
	public void destroy() {
	}

	@Override
	protected void doGet(final HttpServletRequest request, final HttpServletResponse response) {

		final Authenticator auth        = getConfig().getAuthenticator();
		List pages                = null;
		boolean requestUriContainsUuids = false;

		SecurityContext securityContext;
		final App app;

		try {
			final String path = request.getPathInfo();

			// check for registration (has its own tx because of write access
			if (checkRegistration(auth, request, response, path)) {

				return;
			}

			// check for registration (has its own tx because of write access
			if (checkResetPassword(auth, request, response, path)) {

				return;
			}

			// isolate request authentication in a transaction
			try (final Tx tx = StructrApp.getInstance().tx()) {
				securityContext = auth.initializeAndExamineRequest(request, response);
				tx.success();
			}

			app = StructrApp.getInstance(securityContext);

			try (final Tx tx = app.tx()) {

				// Ensure access mode is frontend
				securityContext.setAccessMode(AccessMode.Frontend);

				request.setCharacterEncoding("UTF-8");

				// Important: Set character encoding before calling response.getWriter() !!, see Servlet Spec 5.4
				response.setCharacterEncoding("UTF-8");

				boolean dontCache = false;

				logger.log(Level.FINE, "Path info {0}", path);

				// don't continue on redirects
				if (response.getStatus() == 302) {

					tx.success();
					return;
				}

				final Principal user = securityContext.getUser(false);
				if (user != null) {

					// Don't cache if a user is logged in
					dontCache = true;

				}

				final RenderContext renderContext = RenderContext.getInstance(securityContext, request, response);

				renderContext.setResourceProvider(config.getResourceProvider());

				final EditMode edit = renderContext.getEditMode(user);

				DOMNode rootElement = null;
				AbstractNode dataNode = null;

				final String[] uriParts = PathHelper.getParts(path);
				if ((uriParts == null) || (uriParts.length == 0)) {

					// find a visible page
					rootElement = findIndexPage(securityContext, pages, edit);

					logger.log(Level.FINE, "No path supplied, trying to find index page");

				} else {

					if (rootElement == null) {

						rootElement = findPage(securityContext, pages, path, edit);

					} else {
						dontCache = true;
					}
				}

				if (rootElement == null) { // No page found

					// Look for a file
					final File file = findFile(securityContext, request, path);
					if (file != null) {

						streamFile(securityContext, file, request, response, edit);
						tx.success();
						return;

					}

					// store remaining path parts in request
					final Matcher matcher = threadLocalUUIDMatcher.get();

					for (int i = 0; i < uriParts.length; i++) {

						request.setAttribute(uriParts[i], i);
						matcher.reset(uriParts[i]);

						// set to "true" if part matches UUID pattern
						requestUriContainsUuids |= matcher.matches();

					}

					if (!requestUriContainsUuids) {

						// Try to find a data node by name
						dataNode = findFirstNodeByName(securityContext, request, path);

					} else {

						dataNode = findNodeByUuid(securityContext, PathHelper.getName(path));

					}

					//if (dataNode != null && !(dataNode instanceof Linkable)) {
					if (dataNode != null) {

						// Last path part matches a data node
						// Remove last path part and try again searching for a page
						// clear possible entry points
						request.removeAttribute(POSSIBLE_ENTRY_POINTS_KEY);

						rootElement = findPage(securityContext, pages, StringUtils.substringBeforeLast(path, PathHelper.PATH_SEP), edit);

						renderContext.setDetailsDataObject(dataNode);

						// Start rendering on data node
						if (rootElement == null && dataNode instanceof DOMNode) {

							rootElement = ((DOMNode) dataNode);
						}
					}
				}

				// look for pages with HTTP Basic Authentication (must be done as superuser)
				if (rootElement == null) {

					final HttpBasicAuthResult authResult = checkHttpBasicAuth(request, response, path);

					switch (authResult.authState()) {

						// Element with Basic Auth found and authentication succeeded
						case Authenticated:
							final Linkable result = authResult.getRootElement();
							if (result instanceof Page) {

								rootElement = (DOMNode)result;
								securityContext = authResult.getSecurityContext();
								renderContext.pushSecurityContext(securityContext);

							} else if (result instanceof File) {

								streamFile(authResult.getSecurityContext(), (File)result, request, response, EditMode.NONE);
								tx.success();
								return;

							}
							break;

						// Page with Basic Auth found but not yet authenticated
						case MustAuthenticate:
							tx.success();
							return;

						// no Basic Auth for given path, go on
						case NoBasicAuth:
							break;
					}

				}

				// Still nothing found, do error handling
				if (rootElement == null) {
					rootElement = notFound(response, securityContext);
				}

				if (rootElement == null) {
					tx.success();
					return;
				}

				// check dont cache flag on page (if root element is a page)
				// but don't modify true to false
				dontCache |= rootElement.getProperty(Page.dontCache);

				if (EditMode.WIDGET.equals(edit) || dontCache) {

					setNoCacheHeaders(response);

				}

				if (!securityContext.isVisible(rootElement)) {

					rootElement = notFound(response, securityContext);
					if (rootElement == null) {

						tx.success();
						return;
					}

				} else {

					if (!EditMode.WIDGET.equals(edit) && !dontCache && notModifiedSince(request, response, rootElement, dontCache)) {

						ServletOutputStream out = response.getOutputStream();
						out.flush();
						//response.flushBuffer();
						out.close();

					} else {

						// prepare response
						response.setCharacterEncoding("UTF-8");

						String contentType = rootElement.getProperty(Page.contentType);

						if (contentType == null) {

							// Default
							contentType = "text/html;charset=UTF-8";
						}

						if (contentType.equals("text/html")) {
							contentType = contentType.concat(";charset=UTF-8");
						}

						response.setContentType(contentType);

						setCustomResponseHeaders(response);

						final boolean createsRawData = rootElement.getProperty(Page.pageCreatesRawData);

						// async or not?
						if (isAsync && !createsRawData) {

							final AsyncContext async = request.startAsync();
							final ServletOutputStream out = async.getResponse().getOutputStream();
							final AtomicBoolean finished = new AtomicBoolean(false);
							final DOMNode rootNode = rootElement;

							threadPool.submit(new Runnable() {

								@Override
								public void run() {

									try (final Tx tx = app.tx()) {

										//final long start = System.currentTimeMillis();
										// render
										rootNode.render(renderContext, 0);
										finished.set(true);

										//final long end = System.currentTimeMillis();
										//System.out.println("Done in " + (end-start) + " ms");
										tx.success();

									} catch (Throwable t) {
										logger.log(Level.WARNING, "", t);
										final String errorMsg = t.getMessage();
										try {
											//response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
											response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMsg);
											finished.set(true);
										} catch (IOException ex) {
											logger.log(Level.WARNING, "", ex);
										}
									}
								}

							});

							// start output write listener
							out.setWriteListener(new WriteListener() {

								@Override
								public void onWritePossible() throws IOException {

									try {

										final Queue queue = renderContext.getBuffer().getQueue();
										while (out.isReady()) {

											String buffer = null;

											synchronized (queue) {
												buffer = queue.poll();
											}

											if (buffer != null) {

												out.print(buffer);

											} else {

												if (finished.get()) {

													async.complete();

													// don't overwrite 404 code from error page
													if (response.getStatus() != HttpServletResponse.SC_NOT_FOUND) {
														response.setStatus(HttpServletResponse.SC_OK);
													}

													// prevent this block from being called again
													break;
												}

												Thread.sleep(1);
											}
										}

									} catch (Throwable t) {
										logger.log(Level.WARNING, "", t);
									}
								}

								@Override
								public void onError(Throwable t) {
									logger.log(Level.WARNING, "", t);
								}
							});

						} else {

							final StringRenderBuffer buffer = new StringRenderBuffer();
							renderContext.setBuffer(buffer);

							// render
							rootElement.render(renderContext, 0);

							try {

								response.getOutputStream().write(buffer.getBuffer().toString().getBytes("utf-8"));
								response.getOutputStream().flush();
								response.getOutputStream().close();

							} catch (IOException ioex) {
								logger.log(Level.WARNING, "", ioex);
							}
						}
					}
				}

				tx.success();

			} catch (FrameworkException fex) {
				logger.log(Level.SEVERE, "Exception while processing request", fex);
			}

		} catch (IOException | FrameworkException t) {

			logger.log(Level.SEVERE, "Exception while processing request", t);
			UiAuthenticator.writeInternalServerError(response);
		}
	}

	@Override
	protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);

	}

	@Override
	protected void doHead(final HttpServletRequest request, final HttpServletResponse response) {

		final Authenticator auth = getConfig().getAuthenticator();
		SecurityContext securityContext;
		List pages                = null;
		boolean requestUriContainsUuids = false;
		final App app;

		try {
			String path = request.getPathInfo();

			// isolate request authentication in a transaction
			try (final Tx tx = StructrApp.getInstance().tx()) {
				securityContext = auth.initializeAndExamineRequest(request, response);
				tx.success();
			}

			app = StructrApp.getInstance(securityContext);

			try (final Tx tx = app.tx()) {

				// Ensure access mode is frontend
				securityContext.setAccessMode(AccessMode.Frontend);

				request.setCharacterEncoding("UTF-8");

				// Important: Set character encoding before calling response.getWriter() !!, see Servlet Spec 5.4
				response.setCharacterEncoding("UTF-8");
				response.setContentLength(0);

				boolean dontCache = false;

				logger.log(Level.FINE, "Path info {0}", path);

				// don't continue on redirects
				if (response.getStatus() == 302) {

					tx.success();
					return;
				}

				final Principal user = securityContext.getUser(false);
				if (user != null) {

					// Don't cache if a user is logged in
					dontCache = true;

				}

				final RenderContext renderContext = RenderContext.getInstance(securityContext, request, response);

				renderContext.setResourceProvider(config.getResourceProvider());

				final EditMode edit = renderContext.getEditMode(user);

				DOMNode rootElement = null;
				AbstractNode dataNode = null;

				String[] uriParts = PathHelper.getParts(path);
				if ((uriParts == null) || (uriParts.length == 0)) {

					// find a visible page
					rootElement = findIndexPage(securityContext, pages, edit);

					logger.log(Level.FINE, "No path supplied, trying to find index page");

				} else {

					if (rootElement == null) {

						rootElement = findPage(securityContext, pages, path, edit);

					} else {
						dontCache = true;
					}
				}

				if (rootElement == null) { // No page found

					// Look for a file
					File file = findFile(securityContext, request, path);
					if (file != null) {

						//streamFile(securityContext, file, request, response, edit);
						tx.success();
						return;

					}

					// store remaining path parts in request
					Matcher matcher = threadLocalUUIDMatcher.get();

					for (int i = 0; i < uriParts.length; i++) {

						request.setAttribute(uriParts[i], i);
						matcher.reset(uriParts[i]);

						// set to "true" if part matches UUID pattern
						requestUriContainsUuids |= matcher.matches();

					}

					if (!requestUriContainsUuids) {

						// Try to find a data node by name
						dataNode = findFirstNodeByName(securityContext, request, path);

					} else {

						dataNode = findNodeByUuid(securityContext, PathHelper.getName(path));

					}

					if (dataNode != null && !(dataNode instanceof Linkable)) {

						// Last path part matches a data node
						// Remove last path part and try again searching for a page
						// clear possible entry points
						request.removeAttribute(POSSIBLE_ENTRY_POINTS_KEY);

						rootElement = findPage(securityContext, pages, StringUtils.substringBeforeLast(path, PathHelper.PATH_SEP), edit);

						renderContext.setDetailsDataObject(dataNode);

						// Start rendering on data node
						if (rootElement == null && dataNode instanceof DOMNode) {

							rootElement = ((DOMNode) dataNode);

						}

					}

				}

				// look for pages with HTTP Basic Authentication (must be done as superuser)
				if (rootElement == null) {

					final HttpBasicAuthResult authResult = checkHttpBasicAuth(request, response, path);

					switch (authResult.authState()) {

						// Element with Basic Auth found and authentication succeeded
						case Authenticated:
							final Linkable result = authResult.getRootElement();
							if (result instanceof Page) {

								rootElement = (DOMNode)result;
								renderContext.pushSecurityContext(authResult.getSecurityContext());

							} else if (result instanceof File) {

								//streamFile(authResult.getSecurityContext(), (File)result, request, response, EditMode.NONE);
								tx.success();
								return;

							}
							break;

						// Page with Basic Auth found but not yet authenticated
						case MustAuthenticate:
							return;

						// no Basic Auth for given path, go on
						case NoBasicAuth:
							break;
					}
				}

				// Still nothing found, do error handling
				if (rootElement == null) {

					// Check if security context has set an 401 status
					if (response.getStatus() == HttpServletResponse.SC_UNAUTHORIZED) {

						try {

							UiAuthenticator.writeUnauthorized(response);

						} catch (IllegalStateException ise) {
						}

					} else {

						rootElement = notFound(response, securityContext);

					}

				}

				if (rootElement == null) {

					// no content
					response.setContentLength(0);
					response.getOutputStream().close();

					tx.success();
					return;
				}

				// check dont cache flag on page (if root element is a page)
				// but don't modify true to false
				dontCache |= rootElement.getProperty(Page.dontCache);

				if (EditMode.WIDGET.equals(edit) || dontCache) {

					setNoCacheHeaders(response);

				}

				if (!securityContext.isVisible(rootElement)) {

					rootElement = notFound(response, securityContext);
					if (rootElement == null) {

						tx.success();
						return;
					}

				}

				if (securityContext.isVisible(rootElement)) {

					if (!EditMode.WIDGET.equals(edit) && !dontCache && notModifiedSince(request, response, rootElement, dontCache)) {

						response.getOutputStream().close();

					} else {

						// prepare response
						response.setCharacterEncoding("UTF-8");

						String contentType = rootElement.getProperty(Page.contentType);

						if (contentType == null) {

							// Default
							contentType = "text/html;charset=UTF-8";
						}

						if (contentType.equals("text/html")) {
							contentType = contentType.concat(";charset=UTF-8");
						}

						response.setContentType(contentType);

						setCustomResponseHeaders(response);

						response.getOutputStream().close();
					}

				} else {

					notFound(response, securityContext);

					response.getOutputStream().close();
				}

				tx.success();

			} catch (Throwable fex) {
				logger.log(Level.SEVERE, "Exception while processing request", fex);
			}

		} catch (FrameworkException t) {

			logger.log(Level.SEVERE, "Exception while processing request", t);
			UiAuthenticator.writeInternalServerError(response);
		}
	}

	@Override
	protected void doOptions(final HttpServletRequest request, final HttpServletResponse response) {

		final Authenticator auth = config.getAuthenticator();

		try {

			// isolate request authentication in a transaction
			try (final Tx tx = StructrApp.getInstance().tx()) {
				auth.initializeAndExamineRequest(request, response);
				tx.success();
			}

			response.setContentLength(0);
			response.setHeader("Allow", "GET,HEAD,OPTIONS");

		} catch (FrameworkException t) {

			logger.log(Level.SEVERE, "Exception while processing request", t);
			UiAuthenticator.writeInternalServerError(response);
		}
	}

	/**
	 * Handle 404 Not Found
	 *
	 * First, search the first page which handles the 404.
	 *
	 * If none found, issue the container's 404 error.
	 *
	 * @param response
	 * @param securityContext
	 * @param renderContext
	 * @throws IOException
	 * @throws FrameworkException
	 */
	private Page notFound(final HttpServletResponse response, final SecurityContext securityContext) throws IOException, FrameworkException {

		final Page errorPage = StructrApp.getInstance(securityContext).nodeQuery(Page.class).and(Page.showOnErrorCodes, "404", false).getFirst();

		if (errorPage != null) {

			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return errorPage;

		} else {

			response.sendError(HttpServletResponse.SC_NOT_FOUND);

		}

		return null;

	}

	/**
	 * Find first node whose name matches the last part of the given path
	 *
	 * @param securityContext
	 * @param request
	 * @param path
	 * @return node
	 * @throws FrameworkException
	 */
	private AbstractNode findFirstNodeByName(final SecurityContext securityContext, final HttpServletRequest request, final String path) throws FrameworkException {

		final String name = PathHelper.getName(path);

		if (!name.isEmpty()) {

			logger.log(Level.FINE, "Requested name: {0}", name);

			final Query query                  = StructrApp.getInstance(securityContext).nodeQuery();
			final ConfigurationProvider config = StructrApp.getConfiguration();

			if (!possiblePropertyNamesForEntityResolving.isEmpty()) {

				query.and();
				resolvePossiblePropertyNamesForObjectResolution(config, query, name);
				query.parent();
			}


			final Result results = query.getResult();

			logger.log(Level.FINE, "{0} results", results.size());
			request.setAttribute(POSSIBLE_ENTRY_POINTS_KEY, results.getResults());

			return (results.size() > 0 ? (AbstractNode) results.get(0) : null);
		}

		return null;
	}

	/**
	 * Find node by uuid
	 *
	 * @param securityContext
	 * @param request
	 * @param uuid
	 * @return node
	 * @throws FrameworkException
	 */
	private AbstractNode findNodeByUuid(final SecurityContext securityContext, final String uuid) throws FrameworkException {

		if (!uuid.isEmpty()) {

			logger.log(Level.FINE, "Requested id: {0}", uuid);

			return (AbstractNode) StructrApp.getInstance(securityContext).getNodeById(uuid);
		}

		return null;
	}

	/**
	 * Find a file with its name matching last path part
	 *
	 * @param securityContext
	 * @param request
	 * @param path
	 * @return file
	 * @throws FrameworkException
	 */
	private File findFile(final SecurityContext securityContext, final HttpServletRequest request, final String path) throws FrameworkException {

		List entryPoints = findPossibleEntryPoints(securityContext, request, path);

		// If no results were found, try to replace whitespace by '+' or '%20'
		if (entryPoints.isEmpty()) {
			entryPoints = findPossibleEntryPoints(securityContext, request, PathHelper.replaceWhitespaceByPlus(path));
		}

		if (entryPoints.isEmpty()) {
			entryPoints = findPossibleEntryPoints(securityContext, request, PathHelper.replaceWhitespaceByPercentTwenty(path));
		}

		for (Linkable node : entryPoints) {

			if (node instanceof File && (path.equals(node.getPath()) || node.getUuid().equals(PathHelper.getName(path)))) {
				return (File) node;
			}
		}

		return null;
	}

	/**
	 * Find a page with matching path.
	 *
	 * To be compatible with older versions, fallback to name-only lookup.
	 *
	 * @param securityContext
	 * @param pages
	 * @param path
	 * @param edit
	 * @return page
	 * @throws FrameworkException
	 */
	private Page findPage(final SecurityContext securityContext, List pages, final String path, final EditMode edit) throws FrameworkException {

		if (pages == null) {
			pages = StructrApp.getInstance(securityContext).nodeQuery(Page.class).getAsList();
			Collections.sort(pages, new GraphObjectComparator(Page.position, GraphObjectComparator.ASCENDING));
		}

		for (final Page page : pages) {

			final String pagePath = page.getPath();
			final String name     = PathHelper.getName(path);

			if (((pagePath != null && pagePath.equals(path)) || name.equals(page.getName()) || name.equals(page.getUuid()) ) && (EditMode.CONTENT.equals(edit) || isVisibleForSite(securityContext.getRequest(), page))) {
				return page;
			}
		}

		return null;
	}

	/**
	 * Find the page with the lowest non-empty position value which is visible in the
	 * current security context and for the given site.
	 *
	 * @param securityContext
	 * @param pages
	 * @param edit
	 * @return page
	 * @throws FrameworkException
	 */
	private Page findIndexPage(final SecurityContext securityContext, List pages, final EditMode edit) throws FrameworkException {

		if (pages == null) {
			pages = StructrApp.getInstance(securityContext).nodeQuery(Page.class).getAsList();
			Collections.sort(pages, new GraphObjectComparator(Page.position, GraphObjectComparator.ASCENDING));
		}

		for (Page page : pages) {

			if (securityContext.isVisible(page) && page.getProperty(Page.position) != null && ((EditMode.CONTENT.equals(edit) || isVisibleForSite(securityContext.getRequest(), page)) || (page.getProperty(Page.enableBasicAuth) && page.getProperty(Page.visibleToAuthenticatedUsers)))) {

				return page;
			}
		}

		return null;
	}

	/**
	 * This method checks if the current request is a user registration
	 * confirmation, usually triggered by a user clicking on a confirmation
	 * link in an e-mail.
	 *
	 * @param request
	 * @param response
	 * @param path
	 * @return true if the registration was successful
	 * @throws FrameworkException
	 * @throws IOException
	 */
	private boolean checkRegistration(final Authenticator auth, final HttpServletRequest request, final HttpServletResponse response, final String path) throws FrameworkException, IOException {

		logger.log(Level.FINE, "Checking registration ...");

		String key = request.getParameter(CONFIRM_KEY_KEY);

		if (StringUtils.isEmpty(key)) {
			return false;
		}

		final String targetPage = request.getParameter(TARGET_PAGE_KEY);
		final String errorPage = request.getParameter(ERROR_PAGE_KEY);

		if (CONFIRM_REGISTRATION_PAGE.equals(path)) {

			final App app = StructrApp.getInstance();

			Result results;
			try (final Tx tx = app.tx()) {

				results = app.nodeQuery(Principal.class).and(User.confirmationKey, key).getResult();

				tx.success();
			}

			if (!results.isEmpty()) {

				final Principal user = results.get(0);

				try (final Tx tx = app.tx()) {

					// Clear confirmation key and set session id
					user.setProperty(User.confirmationKey, null);

					if (auth.getUserAutoLogin()) {

						AuthHelper.doLogin(request, user);
					}

					tx.success();
				}

				// Redirect to target page
				if (StringUtils.isNotBlank(targetPage)) {
					response.sendRedirect("/" + targetPage);
				}

				return true;

			} else {
				// Redirect to error page
				if (StringUtils.isNotBlank(errorPage)) {
					response.sendRedirect("/" + errorPage);
				}

				return true;
			}
		}

		return false;
	}

	/**
	 * This method checks if the current request to reset a user password
	 *
	 * @param request
	 * @param response
	 * @param path
	 * @return true if the registration was successful
	 * @throws FrameworkException
	 * @throws IOException
	 */
	private boolean checkResetPassword(final Authenticator auth, final HttpServletRequest request, final HttpServletResponse response, final String path) throws FrameworkException, IOException {

		logger.log(Level.FINE, "Checking registration ...");

		String key = request.getParameter(CONFIRM_KEY_KEY);

		if (StringUtils.isEmpty(key)) {
			return false;
		}

		final String targetPage = request.getParameter(TARGET_PAGE_KEY);

		if (RESET_PASSWORD_PAGE.equals(path)) {

			final App app = StructrApp.getInstance();

			Result results;
			try (final Tx tx = app.tx()) {

				results = app.nodeQuery(Principal.class).and(User.confirmationKey, key).getResult();

				tx.success();
			}

			if (!results.isEmpty()) {

				final Principal user = results.get(0);

				try (final Tx tx = app.tx()) {

					// Clear confirmation key and set session id
					user.setProperty(User.confirmationKey, null);

					if (auth.getUserAutoLogin()) {

						AuthHelper.doLogin(request, user);
					}

					tx.success();
				}
			}

			// Redirect to target page
			if (StringUtils.isNotBlank(targetPage)) {
				response.sendRedirect(targetPage);
			}

			return true;
		}

		return false;
	}

	private List findPossibleEntryPointsByUuid(final SecurityContext securityContext, final HttpServletRequest request, final String uuid) throws FrameworkException {

		final List possibleEntryPoints = (List) request.getAttribute(POSSIBLE_ENTRY_POINTS_KEY);

		if (CollectionUtils.isNotEmpty(possibleEntryPoints)) {
			return possibleEntryPoints;
		}

		if (uuid.length() > 0) {

			logger.log(Level.FINE, "Requested id: {0}", uuid);

			final Query query = StructrApp.getInstance(securityContext).nodeQuery();

			query.and(GraphObject.id, uuid);
			query.and().orType(Page.class).orTypes(File.class);

			// Searching for pages needs super user context anyway
			Result results = query.getResult();

			logger.log(Level.FINE, "{0} results", results.size());
			request.setAttribute(POSSIBLE_ENTRY_POINTS_KEY, results.getResults());

			return (List) results.getResults();
		}

		return Collections.EMPTY_LIST;
	}

	private List findPossibleEntryPointsByPath(final SecurityContext securityContext, final HttpServletRequest request, final String path) throws FrameworkException {

		final List possibleEntryPoints = (List) request.getAttribute(POSSIBLE_ENTRY_POINTS_KEY);

		if (CollectionUtils.isNotEmpty(possibleEntryPoints)) {
			return possibleEntryPoints;
		}

		if (path.length() > 0) {

			logger.log(Level.FINE, "Requested path: {0}", path);

			final Query query = StructrApp.getInstance(securityContext).nodeQuery();

			query.and(Page.path, path);
			query.and().orType(Page.class).orTypes(File.class);

			// Searching for pages needs super user context anyway
			Result results = query.getResult();

			logger.log(Level.FINE, "{0} results", results.size());
			request.setAttribute(POSSIBLE_ENTRY_POINTS_KEY, results.getResults());

			return (List) results.getResults();
		}

		return Collections.EMPTY_LIST;
	}

	private List findPossibleEntryPoints(final SecurityContext securityContext, final HttpServletRequest request, final String path) throws FrameworkException {

		List possibleEntryPoints = (List) request.getAttribute(POSSIBLE_ENTRY_POINTS_KEY);

		if (CollectionUtils.isNotEmpty(possibleEntryPoints)) {
			return possibleEntryPoints;
		}

		final int numberOfParts = PathHelper.getParts(path).length;

		if (numberOfParts > 0) {

			logger.log(Level.FINE, "Requested name {0}", path);

			possibleEntryPoints = findPossibleEntryPointsByPath(securityContext, request, path);

			if (possibleEntryPoints.isEmpty() && numberOfParts == 1) {
				possibleEntryPoints = findPossibleEntryPointsByUuid(securityContext, request, PathHelper.getName(path));
			}

			return possibleEntryPoints;
		}

		return Collections.EMPTY_LIST;
	}

	//~--- set methods ----------------------------------------------------
	public static void setNoCacheHeaders(final HttpServletResponse response) {

		response.setHeader("Cache-Control", "private, max-age=0, s-maxage=0, no-cache, no-store, must-revalidate"); // HTTP 1.1.
		response.setHeader("Pragma", "no-cache, no-store"); // HTTP 1.0.
		response.setDateHeader("Expires", 0);

	}

	private static void setCustomResponseHeaders(final HttpServletResponse response) {

		for (final String header : customResponseHeaders) {

			final String[] keyValuePair = header.split("[ :]+");
			response.setHeader(keyValuePair[0], keyValuePair[1]);

			logger.log(Level.FINE, "Set custom response header: {0} {1}", new Object[]{keyValuePair[0], keyValuePair[1]});

		}

	}

	private static boolean notModifiedSince(final HttpServletRequest request, HttpServletResponse response, final AbstractNode node, final boolean dontCache) {

		boolean notModified = false;
		final Date lastModified = node.getLastModifiedDate();

		// add some caching directives to header
		// see http://weblogs.java.net/blog/2007/08/08/expires-http-header-magic-number-yslow
		final DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
		httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));

		response.setHeader("Date", httpDateFormat.format(new Date()));

		final Calendar cal = new GregorianCalendar();
		final Integer seconds = node.getProperty(Page.cacheForSeconds);

		if (!dontCache && seconds != null) {

			cal.add(Calendar.SECOND, seconds);
			response.setHeader("Cache-Control", "max-age=" + seconds + ", s-maxage=" + seconds + "");
			response.setHeader("Expires", httpDateFormat.format(cal.getTime()));

		} else {

			if (!dontCache) {
				response.setHeader("Cache-Control", "no-cache, must-revalidate, proxy-revalidate");
			} else {
				response.setHeader("Cache-Control", "private, no-cache, no-store, max-age=0, s-maxage=0, must-revalidate, proxy-revalidate");
			}

		}

		if (lastModified != null) {

			final Date roundedLastModified = DateUtils.round(lastModified, Calendar.SECOND);
			response.setHeader("Last-Modified", httpDateFormat.format(roundedLastModified));

			final String ifModifiedSince = request.getHeader("If-Modified-Since");

			if (StringUtils.isNotBlank(ifModifiedSince)) {

				try {

					Date ifModSince = httpDateFormat.parse(ifModifiedSince);

					// Note that ifModSince has not ms resolution, so the last digits are always 000
					// That requires the lastModified to be rounded to seconds
					if ((ifModSince != null) && (roundedLastModified.equals(ifModSince) || roundedLastModified.before(ifModSince))) {

						notModified = true;

						response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
						response.setHeader("Vary", "Accept-Encoding");

					}

				} catch (ParseException ex) {
					logger.log(Level.WARNING, "Could not parse If-Modified-Since header", ex);
				}

			}

		}

		return notModified;
	}

	private void streamFile(SecurityContext securityContext, final File file, HttpServletRequest request, HttpServletResponse response, final EditMode edit) throws IOException {

		if (!securityContext.isVisible(file)) {

			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;

		}

		final ServletOutputStream out         = response.getOutputStream();
		final String downloadAsFilename       = request.getParameter(DOWNLOAD_AS_FILENAME_KEY);
		final Map callbackMap = new LinkedHashMap<>();

		// make edit mode available in callback method
		callbackMap.put("editMode", edit);


		if (downloadAsFilename != null) {
			// Set Content-Disposition header to suggest a default filename and force a "save-as" dialog
			// See:
			// http://en.wikipedia.org/wiki/MIME#Content-Disposition,
			// http://tools.ietf.org/html/rfc2183
			// http://tools.ietf.org/html/rfc1806
			// http://tools.ietf.org/html/rfc2616#section-15.5 and http://tools.ietf.org/html/rfc2616#section-19.5.1
			response.addHeader("Content-Disposition", "attachment; filename=\"" + downloadAsFilename + "\"");

			callbackMap.put("requestedFileName", downloadAsFilename);
		}

		if (!EditMode.WIDGET.equals(edit) && notModifiedSince(request, response, file, false)) {

			out.flush();
			out.close();

			callbackMap.put("statusCode", HttpServletResponse.SC_NOT_MODIFIED);

		} else {

			final String downloadAsDataUrl = request.getParameter(DOWNLOAD_AS_DATA_URL_KEY);
			if (downloadAsDataUrl != null) {

				IOUtils.write(FileHelper.getBase64String(file), out);
				response.setContentType("text/plain");
				response.setStatus(HttpServletResponse.SC_OK);

				out.flush();
				out.close();

				callbackMap.put("statusCode", HttpServletResponse.SC_OK);

			} else {

				// 2b: stream file to response
				final InputStream in = file.getInputStream();
				final String contentType = file.getContentType();

				if (contentType != null) {

					response.setContentType(contentType);

				} else {

					// Default
					response.setContentType("application/octet-stream");
				}

				final String range = request.getHeader("Range");

				try {

					if (StringUtils.isNotEmpty(range)) {

						final long len = file.getSize();
						long start     = 0;
						long end       = len - 1;

						final Matcher matcher = Pattern.compile("bytes=(?\\d*)-(?\\d*)").matcher(range);

						if (matcher.matches()) {
							String startGroup = matcher.group("start");
							start = startGroup.isEmpty() ? start : Long.valueOf(startGroup);
							start = Math.max(0, start);

							String endGroup = matcher.group("end");
							end = endGroup.isEmpty() ? end : Long.valueOf(endGroup);
							end = end > len - 1 ? len - 1 : end;
						}

						long contentLength = end - start + 1;

						// Tell the client that we support byte ranges
						response.setHeader("Accept-Ranges", "bytes");
						response.setHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, len));
						response.setHeader("Content-Length", String.format("%s", contentLength));

						response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
						callbackMap.put("statusCode", HttpServletResponse.SC_PARTIAL_CONTENT);

						IOUtils.copyLarge(in, out, start, contentLength);

					} else {

						response.setStatus(HttpServletResponse.SC_OK);
						callbackMap.put("statusCode", HttpServletResponse.SC_OK);

						IOUtils.copyLarge(in, out);

					}


				} catch (Throwable t) {

				} finally {

					if (out != null) {

						try {
							// 3: output content
							out.flush();
							out.close();

						} catch (Throwable t) {
						}
					}

					if (in != null) {
						in.close();
					}

					response.setStatus(HttpServletResponse.SC_OK);
				}
			}
		}


		// WIDGET mode means "opened in frontend", which we don't want to count as an external download
		if (!EditMode.WIDGET.equals(edit)) {

			// call onDownload callback
			try {

				file.invokeMethod("onDownload", Collections.EMPTY_MAP, false);

			} catch (FrameworkException fex) {
				logger.log(Level.WARNING, "", fex);
			}
		}
	}

	/**
	 * Check if the given page is visible for the requested site defined by
	 * a hostname and a port.
	 *
	 * @param request
	 * @param page
	 * @return
	 */
	private boolean isVisibleForSite(final HttpServletRequest request, final Page page) {

		logger.log(Level.FINE, "Page: {0} [{1}], server name: {2}, server port: {3}", new Object[]{page.getName(), page.getUuid(), request.getServerName(), request.getServerPort()});

		final Site site = page.getProperty(Page.site);

		if (site == null) {
			logger.log(Level.FINE, "Page {0} [{1}] has no site assigned.", new Object[]{page.getName(), page.getUuid()});
			return true;
		}

		logger.log(Level.FINE, "Checking site: {0} [{1}], hostname: {2}, port: {3}", new Object[]{site.getName(), site.getUuid(), site.getProperty(Site.hostname), site.getProperty(Site.port)});

		final String serverName = request.getServerName();
		final int serverPort = request.getServerPort();

		if (StringUtils.isNotBlank(serverName) && !serverName.equals(site.getProperty(Site.hostname))) {
			logger.log(Level.FINE, "Server name {0} does not fit site hostname {1}", new Object[]{serverName, site.getProperty(Site.hostname)});
			return false;
		}

		final Integer sitePort = site.getProperty(Site.port);

		if (sitePort != null && serverPort != sitePort) {
			logger.log(Level.FINE, "Server port {0} does not match site port {1}", new Object[]{serverPort, sitePort});
			return false;
		}

		logger.log(Level.FINE, "Matching site: {0} [{1}], hostname: {2}, port: {3}", new Object[]{site.getName(), site.getUuid(), site.getProperty(Site.hostname), site.getProperty(Site.port)});

		return true;

	}

	private void resolvePossiblePropertyNamesForObjectResolution(final ConfigurationProvider config, final Query query, final String name) {

		for (final String possiblePropertyName : possiblePropertyNamesForEntityResolving) {

			final String[] parts = possiblePropertyName.split("\\.");
			String className     = AbstractNode.class.getSimpleName();
			String keyName       = AbstractNode.name.jsonName();

			switch (parts.length) {

				case 2:
					className = parts[0];
					keyName = parts[1];
					break;

				default:
					logger.log(Level.WARNING, "Unable to process key for object resolution {0}.", possiblePropertyName);
					break;
			}

			if (StringUtils.isNoneBlank(className, keyName)) {

				final Class type = config.getNodeEntityClass(className);
				if (type != null) {

					final PropertyKey key = config.getPropertyKeyForJSONName(type, keyName, false);
					if (key != null) {

						try {

							final PropertyConverter converter = key.inputConverter(SecurityContext.getSuperUserInstance());
							if (converter != null) {

								// try converted value, fail silenty
								query.or(key, converter.convert(name));

							} else {

								// try unconverted value, fail silently if it doesn't work
								query.or(key, name);
							}

						} catch (FrameworkException ignore) { }

					} else {

						logger.log(Level.WARNING, "Unable to find property key {0} of type {1} defined in key {2} used for object resolution.", new Object[] { keyName, className, possiblePropertyName } );
					}

				} else {

					logger.log(Level.WARNING, "Unable to find type {0} defined in key {1} used for object resolution.", new Object[] { className, possiblePropertyName } );
				}
			}
		}
	}

	private HttpBasicAuthResult checkHttpBasicAuth(final HttpServletRequest request, final HttpServletResponse response, final String path) throws IOException, FrameworkException {

		// Look for renderable objects using a SuperUserSecurityContext,
		// but dont actually render the page. We're only interested in
		// the authentication settings.
		Linkable possiblePage = null;

		// try the different methods..
		if (possiblePage == null) {
			possiblePage = StructrApp.getInstance().nodeQuery(Page.class).and(Page.path, path).and(Page.enableBasicAuth, true).sort(Page.position).getFirst();
		}

		if (possiblePage == null) {
			possiblePage = StructrApp.getInstance().nodeQuery(Page.class).and(Page.name, PathHelper.getName(path)).and(Page.enableBasicAuth, true).sort(Page.position).getFirst();
		}

		if (possiblePage == null) {
			possiblePage = StructrApp.getInstance().nodeQuery(File.class).and(File.path, path).and(File.enableBasicAuth, true).getFirst();
		}

		if (possiblePage == null) {
			possiblePage = StructrApp.getInstance().nodeQuery(File.class).and(File.name, PathHelper.getName(path)).and(File.enableBasicAuth, true).getFirst();
		}

		if (possiblePage != null) {

			String realm = possiblePage.getProperty(Page.basicAuthRealm);
			if (realm == null) {

				realm = possiblePage.getName();
			}

			// check Http Basic Authentication headers
			final Principal principal = getPrincipalForAuthorizationHeader(request.getHeader("Authorization"));
			if (principal != null) {

				final SecurityContext securityContext = SecurityContext.getInstance(principal, AccessMode.Frontend);
				if (securityContext != null) {

					// find and instantiate the page again so that the SuperUserSecurityContext
					// can not leak into any of the children of the given page. This is dangerous..
					final Linkable page = StructrApp.getInstance(securityContext).get(Linkable.class, possiblePage.getUuid());
					if (page != null) {

						securityContext.setRequest(request);
						securityContext.setResponse(response);

						return new HttpBasicAuthResult(AuthState.Authenticated, securityContext, page);
					}
				}
			}

			// fallback: the following code will be executed if no Authorization
			// header was sent, OR if the authentication failed
			response.setHeader("WWW-Authenticate", "BASIC realm=\"" + realm + "\"");
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED);

			// no Authorization header sent by client
			return HttpBasicAuthResult.MUST_AUTHENTICATE;
		}

		// no Http Basic Auth enabled for any page
		return HttpBasicAuthResult.NO_BASIC_AUTH;
	}

	private Principal getPrincipalForAuthorizationHeader(final String authHeader) {

		if (authHeader != null) {

			final String[] authParts = authHeader.split(" ");
			if (authParts.length == 2) {

				final String authType  = authParts[0];
				final String authValue = authParts[1];
				String username        = null;
				String password        = null;

				if ("Basic".equals(authType)) {

					final String value   = new String(Base64.decode(authValue), Charset.forName("utf-8"));
					final String[] parts = value.split(":");

					if (parts.length == 2) {

						username = parts[0];
						password = parts[1];
					}
				}

				if (StringUtils.isNoneBlank(username, password)) {

					try {
						return AuthHelper.getPrincipalForPassword(Principal.name, username, password);

					} catch (Throwable t) {
						// ignore
					}
				}
			}
		}

		return null;
	}

	// ----- nested classes -----
	private enum AuthState {
		NoBasicAuth, MustAuthenticate, Authenticated
	}

	private static class HttpBasicAuthResult {

		// use singletons for the most common cases
		public static final HttpBasicAuthResult MUST_AUTHENTICATE = new HttpBasicAuthResult(AuthState.MustAuthenticate);
		public static final HttpBasicAuthResult NO_BASIC_AUTH     = new HttpBasicAuthResult(AuthState.NoBasicAuth);

		private SecurityContext securityContext = null;
		private Linkable rootElement            = null;
		private AuthState authState             = null;

		public HttpBasicAuthResult(final AuthState authState) {
			this(authState, null, null);
		}

		public HttpBasicAuthResult(final AuthState authState, final SecurityContext securityContext, final Linkable rootElement) {

			this.securityContext = securityContext;
			this.rootElement     = rootElement;
			this.authState       = authState;
		}

		public SecurityContext getSecurityContext() {
			return securityContext;
		}

		public AuthState authState() {
			return authState;
		}

		public Linkable getRootElement() {
			return rootElement;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy