blob: c4152c9f6d6d87e329f1f90c6a72706d95d9bb16 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket;
import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.core.request.ClientInfo;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.event.IEventSink;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.FeedbackMessages;
import org.apache.wicket.feedback.IFeedbackContributor;
import org.apache.wicket.page.IPageManager;
import org.apache.wicket.page.PageAccessSynchronizer;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.util.LazyInitializer;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Holds information about a user session, including some fixed number of most recent pages (and all
* their nested component information).
* <ul>
* <li><b>Access</b> - the Session can be retrieved either by {@link Component#getSession()}
* or by directly calling the static method Session.get(). All classes which extend directly or indirectly
* {@link org.apache.wicket.markup.html.WebMarkupContainer} can also use its convenience method
* {@link org.apache.wicket.markup.html.WebMarkupContainer#getWebSession()}</li>
*
* <li><b>Locale</b> - A session has a Locale property to support localization. The Locale for a
* session can be set by calling {@link Session#setLocale(Locale)}. The Locale for a Session
* determines how localized resources are found and loaded.</li>
*
* <li><b>Style</b> - Besides having an appearance based on locale, resources can also have
* different looks in the same locale (a.k.a. "skins"). The style for a session determines the look
* which is used within the appropriate locale. The session style ("skin") can be set with the
* setStyle() method.</li>
*
* <li><b>Resource Loading</b> - Based on the Session locale and style, searching for resources
* occurs in the following order (where sourcePath is set via the ApplicationSettings object for the
* current Application, and style and locale are Session properties):
* <ol>
* <li> [sourcePath]/name[style][locale].[extension]</li>
* <li> [sourcePath]/name[locale].[extension]</li>
* <li> [sourcePath]/name[style].[extension]</li>
* <li> [sourcePath]/name.[extension]</li>
* <li> [classPath]/name[style][locale].[extension]</li>
* <li> [classPath]/name[locale].[extension]</li>
* <li> [classPath]/name[style].[extension]</li>
* <li> [classPath]/name.[extension]</li>
* </ol>
* </li>
*
* <li><b>Session Properties</b> - Arbitrary objects can be attached to a Session by installing a
* session factory on your Application class which creates custom Session subclasses that have
* typesafe properties specific to the application (see {@link Application} for details). To
* discourage non-typesafe access to Session properties, no setProperty() or getProperty() method is
* provided. In a clustered environment, you should take care to call the dirty() method when you
* change a property on your own. This way the session will be reset again in the http session so
* that the http session knows the session is changed.</li>
*
* <li><b>Class Resolver</b> - Sessions have a class resolver ( {@link IClassResolver})
* implementation that is used to locate classes for components such as pages.</li>
*
* <li><b>Page Factory</b> - A pluggable implementation of {@link IPageFactory} is used to
* instantiate pages for the session.</li>
*
* <li><b>Removal</b> - Pages can be removed from the Session forcibly by calling clear(),
* although such an action should rarely be necessary.</li>
*
* <li><b>Flash Messages</b> - Flash messages are messages that are stored in session and are removed
* after they are displayed to the user. Session acts as a store for these messages because they can
* last across requests.</li>
* </ul>
*
* @author Jonathan Locke
* @author Eelco Hillenius
* @author Igor Vaynberg (ivaynberg)
*/
public abstract class Session implements IClusterable, IEventSink, IMetadataContext<Serializable, Session>, IFeedbackContributor
{
private static final long serialVersionUID = 1L;
/** Logging object */
private static final Logger log = LoggerFactory.getLogger(Session.class);
/** records if pages have been unlocked for the current request */
private static final MetaDataKey<Boolean> PAGES_UNLOCKED = new MetaDataKey<>()
{
private static final long serialVersionUID = 1L;
};
/** records if session has been invalidated by the current request */
private static final MetaDataKey<Boolean> SESSION_INVALIDATED = new MetaDataKey<>()
{
private static final long serialVersionUID = 1L;
};
/** Name of session attribute under which this session is stored */
public static final String SESSION_ATTRIBUTE_NAME = "session";
/** a sequence used for whenever something session-specific needs a unique value */
private final AtomicInteger sequence = new AtomicInteger(1);
/** a sequence used for generating page IDs */
private final AtomicInteger pageId = new AtomicInteger(0);
/** synchronize page's access by session */
private final Supplier<PageAccessSynchronizer> pageAccessSynchronizer;
/**
* Checks existence of a <code>Session</code> associated with the current thread.
*
* @return {@code true} if {@link Session#get()} can return the instance of session,
* {@code false} otherwise
*/
public static boolean exists()
{
Session session = ThreadContext.getSession();
if (session == null)
{
// no session is available via ThreadContext, so lookup in session store
RequestCycle requestCycle = RequestCycle.get();
if (requestCycle != null)
{
session = Application.get().getSessionStore().lookup(requestCycle.getRequest());
if (session != null)
{
ThreadContext.setSession(session);
}
}
}
return session != null;
}
/**
* Returns session associated to current thread. Always returns a session during a request
* cycle, even though the session might be temporary
*
* @return session.
*/
public static Session get()
{
Session session = ThreadContext.getSession();
if (session != null)
{
return session;
}
else
{
return Application.get().fetchCreateAndSetSession(RequestCycle.get());
}
}
/**
* Cached instance of agent info which is typically designated by calling
* {@link Session#getClientInfo()}.
*/
protected ClientInfo clientInfo;
/** True if session state has been changed */
private transient volatile boolean dirty = false;
/** feedback messages */
private final FeedbackMessages feedbackMessages = new FeedbackMessages();
/** cached id because you can't access the id after session unbound */
private volatile String id = null;
/** The locale to use when loading resources for this session. */
private final AtomicReference<Locale> locale;
/** Session level meta data. */
private MetaDataEntry<?>[] metaData;
/**
* Temporary instance of the session store. Should be set on each request as it is not supposed
* to go in the session.
*/
private transient ISessionStore sessionStore;
/** Any special "skin" style to use when loading resources. */
private final AtomicReference<String> style = new AtomicReference<>();
/**
* Holds attributes for sessions that are still temporary/ not bound to a session store. Only
* used when {@link #isTemporary()} is true.
* <p>
* Note: this doesn't have to be synchronized, as the only time when this map is used is when a
* session is temporary, in which case it won't be shared between requests (it's a per request
* instance).
* </p>
*/
private transient Map<String, Serializable> temporarySessionAttributes;
/**
* Constructor. Note that {@link RequestCycle} is not available until this constructor returns.
*
* @param request
* The current request
*/
public Session(Request request)
{
Locale locale = request.getLocale();
if (locale == null)
{
throw new IllegalStateException(
"Request#getLocale() cannot return null, request has to have a locale set on it");
}
this.locale = new AtomicReference<>(locale);
pageAccessSynchronizer = new PageAccessSynchronizerProvider();
}
/**
* Force binding this session to the application's {@link ISessionStore session store} if not
* already done so.
* <p>
* A Wicket application can operate in a session-less mode as long as stateless pages are used.
* Session objects will be then created for each request, but they will only live for that
* request. You can recognize temporary sessions by calling {@link #isTemporary()} which
* basically checks whether the session's id is null. Hence, temporary sessions have no session
* id.
* </p>
* <p>
* By calling this method, the session will be bound (made not-temporary) if it was not bound
* yet. It is useful for cases where you want to be absolutely sure this session object will be
* available in next requests. If the session was already bound (
* {@link ISessionStore#lookup(Request) returns a session}), this call will be a noop.
* </p>
*/
public final void bind()
{
// If there is no request cycle then this is not a normal request but for example a last
// modified call.
if (RequestCycle.get() == null)
{
return;
}
ISessionStore store = getSessionStore();
Request request = RequestCycle.get().getRequest();
if (store.lookup(request) == null)
{
// explicitly create a session
id = store.getSessionId(request, true);
// bind it
store.bind(request, this);
if (temporarySessionAttributes != null)
{
for (Map.Entry<String, Serializable> entry : temporarySessionAttributes.entrySet())
{
store.setAttribute(request, entry.getKey(), entry.getValue());
}
temporarySessionAttributes = null;
}
}
}
/**
* Removes all pages from the session. Although this method should rarely be needed, it is
* available (possibly for security reasons).
*/
public final void clear()
{
if (isTemporary() == false)
{
getPageManager().clear();
}
}
/**
* Registers an error feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void error(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.ERROR);
}
/**
* Registers an fatal feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void fatal(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.FATAL);
}
/**
* Registers an debug feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void debug(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.DEBUG);
}
/**
* Get the application that is currently working with this session.
*
* @return Returns the application.
*/
public final Application getApplication()
{
return Application.get();
}
/**
* @return The authorization strategy for this session
*/
public IAuthorizationStrategy getAuthorizationStrategy()
{
return getApplication().getSecuritySettings().getAuthorizationStrategy();
}
/**
* @return The class resolver for this Session
*/
public final IClassResolver getClassResolver()
{
return getApplication().getApplicationSettings().getClassResolver();
}
/**
* Gets the client info object for this session. This method lazily gets the new agent info
* object for this session. It uses any cached or set ({@link #setClientInfo(ClientInfo)})
* client info object.
*
* @return the client info object based on this request
*/
public abstract ClientInfo getClientInfo();
/**
* Gets feedback messages stored in session
*
* @return unmodifiable list of feedback messages
*/
public final FeedbackMessages getFeedbackMessages()
{
return feedbackMessages;
}
/**
* Gets the unique id for this session from the underlying SessionStore. May be
* <code>null</code> if a concrete session is not yet created.
*
* @return The unique id for this session or null if it is a temporary session
*/
public final String getId()
{
if (id == null)
{
updateId();
// we have one?
if (id != null)
{
dirty();
}
}
return id;
}
private void updateId()
{
RequestCycle requestCycle = RequestCycle.get();
if (requestCycle != null)
{
id = getSessionStore().getSessionId(requestCycle.getRequest(), false);
}
}
/**
* Get this session's locale.
*
* @return This session's locale
*/
public Locale getLocale()
{
return locale.get();
}
/**
* Gets metadata for this session using the given key.
*
* @param key
* The key for the data
* @param <M>
* The type of the metadata.
* @return The metadata
* @see MetaDataKey
*/
@Override
public synchronized final <M extends Serializable> M getMetaData(final MetaDataKey<M> key)
{
return key.get(metaData);
}
/**
* @return The page factory for this session
*/
public IPageFactory getPageFactory()
{
return getApplication().getPageFactory();
}
/**
* @return Size of this session
*/
public final long getSizeInBytes()
{
return WicketObjects.sizeof(this);
}
/**
* Get the style (see {@link org.apache.wicket.Session}).
*
* @return Returns the style (see {@link org.apache.wicket.Session})
*/
public final String getStyle()
{
return style.get();
}
/**
* Registers an informational feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void info(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.INFO);
}
/**
* Registers an success feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void success(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.SUCCESS);
}
/**
* Invalidates this session at the end of the current request. If you need to invalidate the
* session immediately, you can do this by calling invalidateNow(), however this will remove all
* Wicket components from this session, which means that you will no longer be able to work with
* them.
*/
public void invalidate()
{
RequestCycle.get().setMetaData(SESSION_INVALIDATED, true);
}
/**
* Invalidate and remove session store and page manager
*/
private void destroy()
{
if (getSessionStore() != null)
{
sessionStore.invalidate(RequestCycle.get().getRequest());
sessionStore = null;
id = null;
RequestCycle.get().setMetaData(SESSION_INVALIDATED, false);
clientInfo = null;
dirty = false;
metaData = null;
}
}
/**
* Invalidates this session immediately. Calling this method will remove all Wicket components
* from this session, which means that you will no longer be able to work with them.
*/
public void invalidateNow()
{
if (isSessionInvalidated() == false)
{
invalidate();
}
// clear all pages possibly pending in the request
getPageManager().clear();
destroy();
feedbackMessages.clear();
setStyle(null);
pageId.set(0);
sequence.set(0);
temporarySessionAttributes = null;
}
/**
* Replaces the underlying (Web)Session, invalidating the current one and creating a new one. By
* calling {@link ISessionStore#invalidate(Request)} and {@link #bind()}
*
* If you are looking for a mean against session fixation attack, consider to use {@link #changeSessionId()}.
*/
public void replaceSession()
{
destroy();
bind();
}
/**
* Whether the session is invalid now, or will be invalidated by the end of the request. Clients
* should rarely need to use this method if ever.
*
* @return Whether the session is invalid when the current request is done
*
* @see #invalidate()
* @see #invalidateNow()
*/
public final boolean isSessionInvalidated()
{
return Boolean.TRUE.equals(RequestCycle.get().getMetaData(SESSION_INVALIDATED));
}
/**
* Whether this session is temporary. A Wicket application can operate in a session-less mode as
* long as stateless pages are used. If this session object is temporary, it will not be
* available on a next request.
*
* @return Whether this session is temporary (which is the same as it's id being null)
*/
public final boolean isTemporary()
{
return getId() == null;
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* Sets the client info object for this session. This will only work when
* {@link #getClientInfo()} is not overridden.
*
* @param clientInfo
* the client info object
*/
public final Session setClientInfo(ClientInfo clientInfo)
{
this.clientInfo = clientInfo;
dirty();
return this;
}
/**
* Set the locale for this session.
*
* @param locale
* New locale
*/
public Session setLocale(final Locale locale)
{
Args.notNull(locale, "locale");
if (!Objects.equal(getLocale(), locale))
{
this.locale.set(locale);
dirty();
}
return this;
}
/**
* Sets the metadata for this session using the given key. If the metadata object is not of the
* correct type for the metadata key, an IllegalArgumentException will be thrown. For
* information on creating MetaDataKeys, see {@link MetaDataKey}.
*
* @param key
* The singleton key for the metadata
* @param object
* The metadata object
* @throws IllegalArgumentException
* @see MetaDataKey
*/
@Override
public final synchronized <M extends Serializable> Session setMetaData(final MetaDataKey<M> key, final M object)
{
metaData = key.set(metaData, object);
dirty();
return this;
}
/**
* Set the style (see {@link org.apache.wicket.Session}).
*
* @param style
* The style to set.
* @return the Session object
*/
public final Session setStyle(final String style)
{
if (!Objects.equal(getStyle(), style))
{
this.style.set(style);
dirty();
}
return this;
}
/**
* Registers a warning feedback message for this session
*
* @param message
* The feedback message
*/
@Override
public final void warn(final Serializable message)
{
addFeedbackMessage(message, FeedbackMessage.WARNING);
}
/**
* Adds a feedback message to the list of messages
*
* @param message
* @param level
*
*/
private void addFeedbackMessage(Serializable message, int level)
{
getFeedbackMessages().add(null, message, level);
dirty();
}
/**
* End the current request.
*/
public void endRequest() {
if (isSessionInvalidated())
{
invalidateNow();
}
else if (!isTemporary())
{
// WICKET-5103 container might have changed id
updateId();
}
}
/**
* Any detach logic for session subclasses. This is called on the end of handling a request,
* when the RequestCycle is about to be detached from the current thread.
*/
public void detach()
{
detachFeedback();
pageAccessSynchronizer.get().unlockAllPages();
RequestCycle.get().setMetaData(PAGES_UNLOCKED, true);
}
private void detachFeedback()
{
final int removed = feedbackMessages.clear(getApplication().getApplicationSettings()
.getFeedbackMessageCleanupFilter());
if (removed != 0)
{
dirty();
}
feedbackMessages.detach();
}
/**
* NOT PART OF PUBLIC API, DO NOT CALL
*
* Detaches internal state of {@link Session}
*/
public void internalDetach()
{
if (dirty)
{
Request request = RequestCycle.get().getRequest();
getSessionStore().flushSession(request, this);
}
dirty = false;
}
/**
* Marks session state as dirty so that it will be (re)stored in the ISessionStore
* at the end of the request.
* <strong>Note</strong>: binds the session if it is temporary
*/
public final void dirty()
{
dirty(true);
}
/**
* Marks session state as dirty so that it will be re-stored in the ISessionStore
* at the end of the request.
*
* @param forced
* A flag indicating whether the session should be marked as dirty even
* when it is temporary. If {@code true} the Session will be bound.
*/
public final void dirty(boolean forced)
{
if (isTemporary())
{
if (forced)
{
dirty = true;
}
}
else
{
dirty = true;
}
}
/**
* Gets the attribute value with the given name
*
* @param name
* The name of the attribute to store
* @return The value of the attribute
*/
public final Serializable getAttribute(final String name)
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle != null)
{
return getSessionStore().getAttribute(cycle.getRequest(), name);
}
}
else
{
if (temporarySessionAttributes != null)
{
return temporarySessionAttributes.get(name);
}
}
return null;
}
/**
* @return List of attributes for this session
*/
public final List<String> getAttributeNames()
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle != null)
{
return Collections.unmodifiableList(getSessionStore().getAttributeNames(
cycle.getRequest()));
}
}
else
{
if (temporarySessionAttributes != null)
{
return Collections.unmodifiableList(new ArrayList<String>(
temporarySessionAttributes.keySet()));
}
}
return Collections.emptyList();
}
/**
* Gets the session store.
*
* @return the session store
*/
protected ISessionStore getSessionStore()
{
if (sessionStore == null)
{
sessionStore = getApplication().getSessionStore();
}
return sessionStore;
}
/**
* Removes the attribute with the given name.
*
* @param name
* the name of the attribute to remove
*/
public final void removeAttribute(String name)
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle != null)
{
getSessionStore().removeAttribute(cycle.getRequest(), name);
}
}
else
{
if (temporarySessionAttributes != null)
{
temporarySessionAttributes.remove(name);
}
}
}
/**
* Adds or replaces the attribute with the given name and value.
*
* @param name
* The name of the attribute
* @param value
* The value of the attribute
*/
public final Session setAttribute(String name, Serializable value)
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle == null)
{
throw new IllegalStateException(
"Cannot set the attribute: no RequestCycle available. If you get this error when using WicketTester.startPage(Page), make sure to call WicketTester.createRequestCycle() beforehand.");
}
ISessionStore store = getSessionStore();
Request request = cycle.getRequest();
// extra check on session binding event
if (value == this)
{
Object current = store.getAttribute(request, name);
if (current == null)
{
String id = store.getSessionId(request, false);
if (id != null)
{
// this is a new instance. wherever it came from, bind
// the session now
store.bind(request, (Session)value);
}
}
}
// Set the actual attribute
store.setAttribute(request, name, value);
}
else
{
// we don't have to synchronize, as it is impossible a temporary
// session instance gets shared across threads
if (temporarySessionAttributes == null)
{
temporarySessionAttributes = new HashMap<>(3);
}
temporarySessionAttributes.put(name, value);
}
return this;
}
/**
* Retrieves the next available session-unique value
*
* @return session-unique value
*/
public int nextSequenceValue()
{
dirty(false);
return sequence.getAndIncrement();
}
/**
*
* @return the next page id
*/
public int nextPageId()
{
dirty(false);
return pageId.getAndIncrement();
}
/**
* Returns the {@link IPageManager} instance.
*
* @return {@link IPageManager} instance.
*/
public final IPageManager getPageManager()
{
if (Boolean.TRUE.equals(RequestCycle.get().getMetaData(PAGES_UNLOCKED))) {
throw new WicketRuntimeException("The request has been processed. Access to pages is no longer allowed");
}
IPageManager manager = Application.get().internalGetPageManager();
return pageAccessSynchronizer.get().adapt(manager);
}
/** {@inheritDoc} */
@Override
public void onEvent(IEvent<?> event)
{
}
/**
* A callback method that is executed when the user session is invalidated
* either by explicit call to {@link org.apache.wicket.Session#invalidate()}
* or due to HttpSession expiration.
*
* <p>In case of session expiration this method is called in a non-worker thread, i.e.
* there are no thread locals exported for the Application, RequestCycle and Session.
* The Session is the current instance. The Application can be found by using
* {@link Application#get(String)}. There is no way to get a reference to a RequestCycle</p>
*/
public void onInvalidate()
{
}
/**
* Change the id of the underlying (Web)Session if this last one is permanent.
* <p>
* Call upon login to protect against session fixation.
*
* @see "http://www.owasp.org/index.php/Session_Fixation"
*/
public void changeSessionId()
{
if (isTemporary())
{
return;
}
id = generateNewSessionId();
}
/**
* Change the id of the underlying (Web)Session.
*
* @return the new session id value.
*/
protected abstract String generateNewSessionId();
/**
* Factory method for PageAccessSynchronizer instances
*
* @param timeout
* The configured timeout. See {@link org.apache.wicket.settings.RequestCycleSettings#getTimeout()}
* @return A new instance of PageAccessSynchronizer
*/
protected PageAccessSynchronizer newPageAccessSynchronizer(Duration timeout)
{
return new PageAccessSynchronizer(timeout);
}
private final class PageAccessSynchronizerProvider extends LazyInitializer<PageAccessSynchronizer>
{
private static final long serialVersionUID = 1L;
@Override
protected PageAccessSynchronizer createInstance()
{
final Duration timeout;
if (Application.exists())
{
timeout = Application.get().getRequestCycleSettings().getTimeout();
}
else
{
timeout = Duration.ofMinutes(1);
log.warn(
"PageAccessSynchronizer created outside of application thread, using default timeout: {}",
timeout);
}
return newPageAccessSynchronizer(timeout);
}
}
}