001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket;
018
019import java.io.Serializable;
020import java.time.Duration;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.concurrent.atomic.AtomicInteger;
028import java.util.concurrent.atomic.AtomicReference;
029import java.util.function.Supplier;
030import java.util.regex.Pattern;
031
032import org.apache.wicket.application.IClassResolver;
033import org.apache.wicket.authorization.IAuthorizationStrategy;
034import org.apache.wicket.core.request.ClientInfo;
035import org.apache.wicket.core.util.lang.WicketObjects;
036import org.apache.wicket.event.IEvent;
037import org.apache.wicket.event.IEventSink;
038import org.apache.wicket.feedback.FeedbackMessage;
039import org.apache.wicket.feedback.FeedbackMessages;
040import org.apache.wicket.feedback.IFeedbackContributor;
041import org.apache.wicket.page.IPageManager;
042import org.apache.wicket.page.PageAccessSynchronizer;
043import org.apache.wicket.request.Request;
044import org.apache.wicket.request.cycle.RequestCycle;
045import org.apache.wicket.session.ISessionStore;
046import org.apache.wicket.util.LazyInitializer;
047import org.apache.wicket.util.io.IClusterable;
048import org.apache.wicket.util.lang.Args;
049import org.apache.wicket.util.lang.Objects;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053
054/**
055 * Holds information about a user session, including some fixed number of most recent pages (and all
056 * their nested component information).
057 * <ul>
058 * <li><b>Access</b> - the Session can be retrieved either by {@link Component#getSession()}
059 * or by directly calling the static method Session.get(). All classes which extend directly or indirectly
060 * {@link org.apache.wicket.markup.html.WebMarkupContainer} can also use its convenience method
061 * {@link org.apache.wicket.markup.html.WebMarkupContainer#getWebSession()}</li>
062 * 
063 * <li><b>Locale</b> - A session has a Locale property to support localization. The Locale for a
064 * session can be set by calling {@link Session#setLocale(Locale)}. The Locale for a Session
065 * determines how localized resources are found and loaded.</li>
066 * 
067 * <li><b>Style</b> - Besides having an appearance based on locale, resources can also have
068 * different looks in the same locale (a.k.a. "skins"). The style for a session determines the look
069 * which is used within the appropriate locale. The session style ("skin") can be set with the
070 * setStyle() method.</li>
071 * 
072 * <li><b>Resource Loading</b> - Based on the Session locale and style, searching for resources
073 * occurs in the following order (where sourcePath is set via the ApplicationSettings object for the
074 * current Application, and style and locale are Session properties):
075 * <ol>
076 * <li> [sourcePath]/name[style][locale].[extension]</li>
077 * <li> [sourcePath]/name[locale].[extension]</li>
078 * <li> [sourcePath]/name[style].[extension]</li>
079 * <li> [sourcePath]/name.[extension]</li>
080 * <li> [classPath]/name[style][locale].[extension]</li>
081 * <li> [classPath]/name[locale].[extension]</li>
082 * <li> [classPath]/name[style].[extension]</li>
083 * <li> [classPath]/name.[extension]</li>
084 * </ol>
085 * </li>
086 * 
087 * <li><b>Session Properties</b> - Arbitrary objects can be attached to a Session by installing a
088 * session factory on your Application class which creates custom Session subclasses that have
089 * typesafe properties specific to the application (see {@link Application} for details). To
090 * discourage non-typesafe access to Session properties, no setProperty() or getProperty() method is
091 * provided. In a clustered environment, you should take care to call the dirty() method when you
092 * change a property on your own. This way the session will be reset again in the http session so
093 * that the http session knows the session is changed.</li>
094 * 
095 * <li><b>Class Resolver</b> - Sessions have a class resolver ( {@link IClassResolver})
096 * implementation that is used to locate classes for components such as pages.</li>
097 * 
098 * <li><b>Page Factory</b> - A pluggable implementation of {@link IPageFactory} is used to
099 * instantiate pages for the session.</li>
100 * 
101 * <li><b>Removal</b> - Pages can be removed from the Session forcibly by calling clear(),
102 * although such an action should rarely be necessary.</li>
103 * 
104 * <li><b>Flash Messages</b> - Flash messages are messages that are stored in session and are removed
105 * after they are displayed to the user. Session acts as a store for these messages because they can
106 * last across requests.</li>
107 * </ul>
108 * 
109 * @author Jonathan Locke
110 * @author Eelco Hillenius
111 * @author Igor Vaynberg (ivaynberg)
112 */
113public abstract class Session implements IClusterable, IEventSink, IMetadataContext<Serializable, Session>, IFeedbackContributor
114{
115        private static final long serialVersionUID = 1L;
116
117        /** Logging object */
118        private static final Logger log = LoggerFactory.getLogger(Session.class);
119
120        /** records if pages have been unlocked for the current request */
121        private static final MetaDataKey<Boolean> PAGES_UNLOCKED = new MetaDataKey<>()
122        {
123                private static final long serialVersionUID = 1L;
124        };
125        
126        /** records if session has been invalidated by the current request */
127        private static final MetaDataKey<Boolean> SESSION_INVALIDATED = new MetaDataKey<>()
128        {
129                private static final long serialVersionUID = 1L;
130        };
131
132        /** Name of session attribute under which this session is stored */
133        public static final String SESSION_ATTRIBUTE_NAME = "session";
134
135        /**
136         * taken from Google Closure Templates BidiUtils
137         *
138         * A regular expression for matching right-to-left language codes. See
139         * {@link #isRtlLanguage} for the design.
140         */
141        private static final Pattern RTL_LOCALE_RE = Pattern.compile("^(ar|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Arab|Hebr|Thaa|Nkoo|Tfng))"
142                                        + "(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)");
143
144        /** a sequence used for whenever something session-specific needs a unique value */
145        private final AtomicInteger sequence = new AtomicInteger(1);
146
147        /** a sequence used for generating page IDs */
148        private final AtomicInteger pageId = new AtomicInteger(0);
149
150        /** synchronize page's access by session */
151        private final Supplier<PageAccessSynchronizer> pageAccessSynchronizer;
152
153        /**
154         * Checks existence of a <code>Session</code> associated with the current thread.
155         * 
156         * @return {@code true} if {@link Session#get()} can return the instance of session,
157         *         {@code false} otherwise
158         */
159        public static boolean exists()
160        {
161                Session session = ThreadContext.getSession();
162
163                if (session == null)
164                {
165                        // no session is available via ThreadContext, so lookup in session store
166                        RequestCycle requestCycle = RequestCycle.get();
167                        if (requestCycle != null)
168                        {
169                                session = Application.get().getSessionStore().lookup(requestCycle.getRequest());
170                                if (session != null)
171                                {
172                                        ThreadContext.setSession(session);
173                                }
174                        }
175                }
176                return session != null;
177        }
178
179        /**
180         * Returns session associated to current thread. Always returns a session during a request
181         * cycle, even though the session might be temporary
182         * 
183         * @return session.
184         */
185        public static Session get()
186        {
187                Session session = ThreadContext.getSession();
188                if (session != null)
189                {
190                        return session;
191                }
192                else
193                {
194                        return Application.get().fetchCreateAndSetSession(RequestCycle.get());
195                }
196        }
197
198        /**
199         * Check if a BCP 47 / III language code indicates an RTL (right-to-left) language, i.e.
200         * either: - a language code explicitly specifying one of the right-to-left
201         * scripts, e.g. "az-Arab", or
202         * <p>
203         * - a language code specifying one of the languages normally written in a
204         * right-to-left script, e.g. "fa" (Farsi), except ones explicitly
205         * specifying Latin or Cyrillic script (which are the usual LTR (left-to-right)
206         * alternatives).
207         * <p>
208         * <a href="http://www.unicode.org/iso15924/iso15924-num.html">
209         * The list of right-to-left scripts appears in the 100-199 range in</a>, of which Arabic and
210         * Hebrew are by far the most widely used. We also recognize Thaana, N'Ko,
211         * and Tifinagh, which also have significant modern usage. The rest (Syriac,
212         * Samaritan, Mandaic, etc.) seem to have extremely limited or no modern
213         * usage and are not recognized. The languages usually written in a
214         * right-to-left script are taken as those with 
215         * <a href="http://www.iana.org/assignments/language-subtag-registry">Suppress-Script</a>:
216         * Hebr|Arab|Thaa|Nkoo|Tfng, as well as
217         * Sindhi (sd) and Uyghur (ug). The presence of other subtags of the
218         * language code, e.g. regions like EG (Egypt), is ignored.
219         *
220         * @param locale - locale to check
221         * @return <code>true</code> in case passed locale is right-to-left
222         */
223        public static boolean isRtlLanguage(final Locale locale) {
224                Args.notNull(locale, "locale");
225                return RTL_LOCALE_RE.matcher(locale.toLanguageTag()).find();
226        }
227
228        /**
229         * Cached instance of agent info which is typically designated by calling
230         * {@link Session#getClientInfo()}.
231         */
232        protected ClientInfo clientInfo;
233
234        /** True if session state has been changed */
235        private transient volatile boolean dirty = false;
236
237        /** feedback messages */
238        private final FeedbackMessages feedbackMessages = new FeedbackMessages();
239
240        /** cached id because you can't access the id after session unbound */
241        private volatile String id = null;
242
243        /** The locale to use when loading resources for this session. */
244        private final AtomicReference<Locale> locale;
245
246        /** True if locale's language is RTL (right-to-left) */
247        private boolean rtlLocale = false;
248
249        /** Session level meta data. */
250        private MetaDataEntry<?>[] metaData;
251
252        /**
253         * Temporary instance of the session store. Should be set on each request as it is not supposed
254         * to go in the session.
255         */
256        private transient ISessionStore sessionStore;
257
258        /** Any special "skin" style to use when loading resources. */
259        private final AtomicReference<String> style = new AtomicReference<>();
260
261        /**
262         * Holds attributes for sessions that are still temporary/ not bound to a session store. Only
263         * used when {@link #isTemporary()} is true.
264         * <p>
265         * Note: this doesn't have to be synchronized, as the only time when this map is used is when a
266         * session is temporary, in which case it won't be shared between requests (it's a per request
267         * instance).
268         * </p>
269         */
270        private transient Map<String, Serializable> temporarySessionAttributes;
271
272        /**
273         * Constructor. Note that {@link RequestCycle} is not available until this constructor returns.
274         * 
275         * @param request
276         *            The current request
277         */
278        public Session(Request request)
279        {
280                Locale locale = request.getLocale();
281                if (locale == null)
282                {
283                        throw new IllegalStateException(
284                                "Request#getLocale() cannot return null, request has to have a locale set on it");
285                }
286                this.locale = new AtomicReference<>(locale);
287                rtlLocale = isRtlLanguage(locale);
288
289                pageAccessSynchronizer = new PageAccessSynchronizerProvider();
290        }
291
292        /**
293         * Force binding this session to the application's {@link ISessionStore session store} if not
294         * already done so.
295         * <p>
296         * A Wicket application can operate in a session-less mode as long as stateless pages are used.
297         * Session objects will be then created for each request, but they will only live for that
298         * request. You can recognize temporary sessions by calling {@link #isTemporary()} which
299         * basically checks whether the session's id is null. Hence, temporary sessions have no session
300         * id.
301         * </p>
302         * <p>
303         * By calling this method, the session will be bound (made not-temporary) if it was not bound
304         * yet. It is useful for cases where you want to be absolutely sure this session object will be
305         * available in next requests. If the session was already bound (
306         * {@link ISessionStore#lookup(Request) returns a session}), this call will be a noop.
307         * </p>
308         */
309        public final void bind()
310        {
311                // If there is no request cycle then this is not a normal request but for example a last
312                // modified call.
313                if (RequestCycle.get() == null)
314                {
315                        return;
316                }
317
318                ISessionStore store = getSessionStore();
319                Request request = RequestCycle.get().getRequest();
320                if (store.lookup(request) == null)
321                {
322                        // explicitly create a session
323                        id = store.getSessionId(request, true);
324                        // bind it
325                        store.bind(request, this);
326
327                        if (temporarySessionAttributes != null)
328                        {
329                                for (Map.Entry<String, Serializable> entry : temporarySessionAttributes.entrySet())
330                                {
331                                        store.setAttribute(request, entry.getKey(), entry.getValue());
332                                }
333                                temporarySessionAttributes = null;
334                        }
335                }
336        }
337
338        /**
339         * Removes all pages from the session. Although this method should rarely be needed, it is
340         * available (possibly for security reasons).
341         */
342        public final void clear()
343        {
344                if (isTemporary() == false)
345                {
346                        getPageManager().clear();
347                }
348        }
349
350        /**
351         * Registers an error feedback message for this session
352         * 
353         * @param message
354         *            The feedback message
355         */
356        @Override
357        public final void error(final Serializable message)
358        {
359                addFeedbackMessage(message, FeedbackMessage.ERROR);
360        }
361
362        /**
363         * Registers an fatal feedback message for this session
364         * 
365         * @param message
366         *            The feedback message
367         */
368        @Override
369        public final void fatal(final Serializable message)
370        {
371                addFeedbackMessage(message, FeedbackMessage.FATAL);
372        }
373
374        /**
375         * Registers an debug feedback message for this session
376         * 
377         * @param message
378         *            The feedback message
379         */
380        @Override
381        public final void debug(final Serializable message)
382        {
383                addFeedbackMessage(message, FeedbackMessage.DEBUG);
384        }
385
386        /**
387         * Get the application that is currently working with this session.
388         * 
389         * @return Returns the application.
390         */
391        public final Application getApplication()
392        {
393                return Application.get();
394        }
395
396        /**
397         * @return The authorization strategy for this session
398         */
399        public IAuthorizationStrategy getAuthorizationStrategy()
400        {
401                return getApplication().getSecuritySettings().getAuthorizationStrategy();
402        }
403
404        /**
405         * @return The class resolver for this Session
406         */
407        public final IClassResolver getClassResolver()
408        {
409                return getApplication().getApplicationSettings().getClassResolver();
410        }
411
412        /**
413         * Gets the client info object for this session. This method lazily gets the new agent info
414         * object for this session. It uses any cached or set ({@link #setClientInfo(ClientInfo)})
415         * client info object.
416         * 
417         * @return the client info object based on this request
418         */
419        public abstract ClientInfo getClientInfo();
420
421        /**
422         * Gets feedback messages stored in session
423         * 
424         * @return unmodifiable list of feedback messages
425         */
426        public final FeedbackMessages getFeedbackMessages()
427        {
428                return feedbackMessages;
429        }
430
431        /**
432         * Gets the unique id for this session from the underlying SessionStore. May be
433         * <code>null</code> if a concrete session is not yet created.
434         * 
435         * @return The unique id for this session or null if it is a temporary session
436         */
437        public final String getId()
438        {
439                if (id == null)
440                {
441                        updateId();
442
443                        // we have one?
444                        if (id != null)
445                        {
446                                dirty();
447                        }
448                }
449                return id;
450        }
451
452        private void updateId()
453        {
454                RequestCycle requestCycle = RequestCycle.get();
455                if (requestCycle != null)
456                {
457                        id = getSessionStore().getSessionId(requestCycle.getRequest(), false);
458                }
459        }
460
461        /**
462         * Get this session's locale.
463         * 
464         * @return This session's locale
465         */
466        public Locale getLocale()
467        {
468                return locale.get();
469        }
470
471        /**
472         * Gets metadata for this session using the given key.
473         * 
474         * @param key
475         *            The key for the data
476         * @param <M>
477         *            The type of the metadata.
478         * @return The metadata
479         * @see MetaDataKey
480         */
481        @Override
482        public synchronized final <M extends Serializable> M getMetaData(final MetaDataKey<M> key)
483        {
484                return key.get(metaData);
485        }
486
487        /**
488         * @return The page factory for this session
489         */
490        public IPageFactory getPageFactory()
491        {
492                return getApplication().getPageFactory();
493        }
494
495        /**
496         * @return Size of this session
497         */
498        public final long getSizeInBytes()
499        {
500                return WicketObjects.sizeof(this);
501        }
502
503        /**
504         * Get the style (see {@link org.apache.wicket.Session}).
505         * 
506         * @return Returns the style (see {@link org.apache.wicket.Session})
507         */
508        public final String getStyle()
509        {
510                return style.get();
511        }
512
513        /**
514         * Registers an informational feedback message for this session
515         * 
516         * @param message
517         *            The feedback message
518         */
519        @Override
520        public final void info(final Serializable message)
521        {
522                addFeedbackMessage(message, FeedbackMessage.INFO);
523        }
524
525        /**
526         * Registers an success feedback message for this session
527         * 
528         * @param message
529         *            The feedback message
530         */
531        @Override
532        public final void success(final Serializable message)
533        {
534                addFeedbackMessage(message, FeedbackMessage.SUCCESS);
535        }
536
537        /**
538         * Invalidates this session at the end of the current request. If you need to invalidate the
539         * session immediately, you can do this by calling invalidateNow(), however this will remove all
540         * Wicket components from this session, which means that you will no longer be able to work with
541         * them.
542         */
543        public void invalidate()
544        {
545                RequestCycle.get().setMetaData(SESSION_INVALIDATED, true);
546        }
547
548        /**
549         * Invalidate and remove session store and page manager
550         */
551        private void destroy()
552        {
553                if (getSessionStore() != null)
554                {
555                        sessionStore.invalidate(RequestCycle.get().getRequest());
556                        sessionStore = null;
557                        id = null;
558                        RequestCycle.get().setMetaData(SESSION_INVALIDATED, false);
559                        clientInfo = null;
560                        dirty = false;
561                        metaData = null;
562                }
563        }
564
565        /**
566         * Invalidates this session immediately. Calling this method will remove all Wicket components
567         * from this session, which means that you will no longer be able to work with them.
568         */
569        public void invalidateNow()
570        {
571                if (isSessionInvalidated() == false) 
572                {
573                        invalidate();
574                }
575                
576                // clear all pages possibly pending in the request
577                getPageManager().clear();
578                
579                destroy();
580                feedbackMessages.clear();
581                setStyle(null);
582                pageId.set(0);
583                sequence.set(0);
584                temporarySessionAttributes = null;
585        }
586
587        /**
588         * Replaces the underlying (Web)Session, invalidating the current one and creating a new one. By
589         * calling {@link ISessionStore#invalidate(Request)} and {@link #bind()}
590         * 
591         * If you are looking for a mean against session fixation attack, consider to use {@link #changeSessionId()}.
592         */
593        public void replaceSession()
594        {
595                destroy();
596                bind();
597        }
598
599        /**
600         * Whether the session is invalid now, or will be invalidated by the end of the request. Clients
601         * should rarely need to use this method if ever.
602         * 
603         * @return Whether the session is invalid when the current request is done
604         * 
605         * @see #invalidate()
606         * @see #invalidateNow()
607         */
608        public final boolean isSessionInvalidated()
609        {
610                return Boolean.TRUE.equals(RequestCycle.get().getMetaData(SESSION_INVALIDATED));
611        }
612
613        /**
614         * Whether this session is temporary. A Wicket application can operate in a session-less mode as
615         * long as stateless pages are used. If this session object is temporary, it will not be
616         * available on a next request.
617         * 
618         * @return Whether this session is temporary (which is the same as it's id being null)
619         */
620        public final boolean isTemporary()
621        {
622                return getId() == null;
623        }
624
625        /**
626         * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
627         * <p>
628         * Sets the client info object for this session. This will only work when
629         * {@link #getClientInfo()} is not overridden.
630         * 
631         * @param clientInfo
632         *            the client info object
633         */
634        public final Session setClientInfo(ClientInfo clientInfo)
635        {
636                this.clientInfo = clientInfo;
637                dirty();
638                return this;
639        }
640
641        /**
642         * Set the locale for this session.
643         * 
644         * @param locale
645         *            New locale
646         */
647        public Session setLocale(final Locale locale)
648        {
649                Args.notNull(locale, "locale");
650
651                if (!Objects.equal(getLocale(), locale))
652                {
653                        this.locale.set(locale);
654                        rtlLocale = isRtlLanguage(locale);
655                        dirty();
656                }
657                return this;
658        }
659
660        /**
661         * Method to determine if language of current locale is RTL (right-to-left) or not
662         *
663         * @return <code>true</code> if language of session locale is RTL (right-to-left), <code>false</code> otherwise
664         */
665        public boolean isRtlLocale() {
666                return rtlLocale;
667        }
668
669        /**
670         * Sets the metadata for this session using the given key. If the metadata object is not of the
671         * correct type for the metadata key, an IllegalArgumentException will be thrown. For
672         * information on creating MetaDataKeys, see {@link MetaDataKey}.
673         * 
674         * @param key
675         *            The singleton key for the metadata
676         * @param object
677         *            The metadata object
678         * @throws IllegalArgumentException
679         * @see MetaDataKey
680         */
681        @Override
682        public final synchronized <M extends Serializable> Session setMetaData(final MetaDataKey<M> key, final M object)
683        {
684                metaData = key.set(metaData, object);
685                dirty();
686                return this;
687        }
688
689        /**
690         * Set the style (see {@link org.apache.wicket.Session}).
691         * 
692         * @param style
693         *            The style to set.
694         * @return the Session object
695         */
696        public final Session setStyle(final String style)
697        {
698                if (!Objects.equal(getStyle(), style))
699                {
700                        this.style.set(style);
701                        dirty();
702                }
703                return this;
704        }
705
706        /**
707         * Registers a warning feedback message for this session
708         * 
709         * @param message
710         *            The feedback message
711         */
712        @Override
713        public final void warn(final Serializable message)
714        {
715                addFeedbackMessage(message, FeedbackMessage.WARNING);
716        }
717
718        /**
719         * Adds a feedback message to the list of messages
720         * 
721         * @param message
722         * @param level
723         * 
724         */
725        private void addFeedbackMessage(Serializable message, int level)
726        {
727                getFeedbackMessages().add(null, message, level);
728                dirty();
729        }
730
731        /**
732         * End the current request.
733         */
734        public void endRequest() {
735                if (isSessionInvalidated())
736                {
737                        invalidateNow();
738                }
739                else if (!isTemporary())
740                {
741                        // WICKET-5103 container might have changed id
742                        updateId();
743                }
744        }
745        
746        /**
747         * Any detach logic for session subclasses. This is called on the end of handling a request,
748         * when the RequestCycle is about to be detached from the current thread.
749         */
750        public void detach()
751        {
752                detachFeedback();
753
754                pageAccessSynchronizer.get().unlockAllPages();
755                RequestCycle.get().setMetaData(PAGES_UNLOCKED, true);
756        }
757
758        private void detachFeedback()
759        {
760                final int removed = feedbackMessages.clear(getApplication().getApplicationSettings()
761                        .getFeedbackMessageCleanupFilter());
762
763                if (removed != 0)
764                {
765                        dirty();
766                }
767
768                feedbackMessages.detach();
769        }
770
771        /**
772         * NOT PART OF PUBLIC API, DO NOT CALL
773         * 
774         * Detaches internal state of {@link Session}
775         */
776        public void internalDetach()
777        {
778                if (dirty)
779                {
780                        Request request = RequestCycle.get().getRequest();
781                        getSessionStore().flushSession(request, this);
782                }
783                dirty = false;
784        }
785
786        /**
787         * Marks session state as dirty so that it will be (re)stored in the ISessionStore
788         * at the end of the request.
789         * <strong>Note</strong>: binds the session if it is temporary
790         */
791        public final void dirty()
792        {
793                dirty(true);
794        }
795
796        /**
797         * Marks session state as dirty so that it will be re-stored in the ISessionStore
798         * at the end of the request.
799         *
800         * @param forced
801         *          A flag indicating whether the session should be marked as dirty even
802         *          when it is temporary. If {@code true} the Session will be bound.
803         */
804        public final void dirty(boolean forced)
805        {
806                if (isTemporary())
807                {
808                        if (forced)
809                        {
810                                dirty = true;
811                        }
812                }
813                else
814                {
815                        dirty = true;
816                }
817        }
818
819        /**
820         * Gets the attribute value with the given name
821         * 
822         * @param name
823         *            The name of the attribute to store
824         * @return The value of the attribute
825         */
826        public final Serializable getAttribute(final String name)
827        {
828                if (!isTemporary())
829                {
830                        RequestCycle cycle = RequestCycle.get();
831                        if (cycle != null)
832                        {
833                                return getSessionStore().getAttribute(cycle.getRequest(), name);
834                        }
835                }
836                else
837                {
838                        if (temporarySessionAttributes != null)
839                        {
840                                return temporarySessionAttributes.get(name);
841                        }
842                }
843                return null;
844        }
845
846        /**
847         * @return List of attributes for this session
848         */
849        public final List<String> getAttributeNames()
850        {
851                if (!isTemporary())
852                {
853                        RequestCycle cycle = RequestCycle.get();
854                        if (cycle != null)
855                        {
856                                return Collections.unmodifiableList(getSessionStore().getAttributeNames(
857                                        cycle.getRequest()));
858                        }
859                }
860                else
861                {
862                        if (temporarySessionAttributes != null)
863                        {
864                                return Collections.unmodifiableList(new ArrayList<String>(
865                                        temporarySessionAttributes.keySet()));
866                        }
867                }
868                return Collections.emptyList();
869        }
870
871        /**
872         * Gets the session store.
873         * 
874         * @return the session store
875         */
876        protected ISessionStore getSessionStore()
877        {
878                if (sessionStore == null)
879                {
880                        sessionStore = getApplication().getSessionStore();
881                }
882                return sessionStore;
883        }
884
885        /**
886         * Removes the attribute with the given name.
887         * 
888         * @param name
889         *            the name of the attribute to remove
890         */
891        public final void removeAttribute(String name)
892        {
893                if (!isTemporary())
894                {
895                        RequestCycle cycle = RequestCycle.get();
896                        if (cycle != null)
897                        {
898                                getSessionStore().removeAttribute(cycle.getRequest(), name);
899                        }
900                }
901                else
902                {
903                        if (temporarySessionAttributes != null)
904                        {
905                                temporarySessionAttributes.remove(name);
906                        }
907                }
908        }
909
910        /**
911         * Adds or replaces the attribute with the given name and value.
912         * 
913         * @param name
914         *            The name of the attribute
915         * @param value
916         *            The value of the attribute
917         */
918        public final Session setAttribute(String name, Serializable value)
919        {
920                if (!isTemporary())
921                {
922                        RequestCycle cycle = RequestCycle.get();
923                        if (cycle == null)
924                        {
925                                throw new IllegalStateException(
926                                        "Cannot set the attribute: no RequestCycle available.  If you get this error when using WicketTester.startPage(Page), make sure to call WicketTester.createRequestCycle() beforehand.");
927                        }
928
929                        ISessionStore store = getSessionStore();
930                        Request request = cycle.getRequest();
931
932                        // extra check on session binding event
933                        if (value == this)
934                        {
935                                Object current = store.getAttribute(request, name);
936                                if (current == null)
937                                {
938                                        String id = store.getSessionId(request, false);
939                                        if (id != null)
940                                        {
941                                                // this is a new instance. wherever it came from, bind
942                                                // the session now
943                                                store.bind(request, (Session)value);
944                                        }
945                                }
946                        }
947
948                        // Set the actual attribute
949                        store.setAttribute(request, name, value);
950                }
951                else
952                {
953                        // we don't have to synchronize, as it is impossible a temporary
954                        // session instance gets shared across threads
955                        if (temporarySessionAttributes == null)
956                        {
957                                temporarySessionAttributes = new HashMap<>(3);
958                        }
959                        temporarySessionAttributes.put(name, value);
960                }
961                return this;
962        }
963
964        /**
965         * Retrieves the next available session-unique value
966         * 
967         * @return session-unique value
968         */
969        public int nextSequenceValue()
970        {
971                dirty(false);
972                return sequence.getAndIncrement();
973        }
974
975        /**
976         * 
977         * @return the next page id
978         */
979        public int nextPageId()
980        {
981                dirty(false);
982                return pageId.getAndIncrement();
983        }
984
985        /**
986         * Returns the {@link IPageManager} instance.
987         * 
988         * @return {@link IPageManager} instance.
989         */
990        public final IPageManager getPageManager()
991        {
992                if (Boolean.TRUE.equals(RequestCycle.get().getMetaData(PAGES_UNLOCKED))) {
993                        throw new WicketRuntimeException("The request has been processed. Access to pages is no longer allowed");
994                }
995                
996                IPageManager manager = Application.get().internalGetPageManager();
997                return pageAccessSynchronizer.get().adapt(manager);
998        }
999
1000        /** {@inheritDoc} */
1001        @Override
1002        public void onEvent(IEvent<?> event)
1003        {
1004        }
1005
1006        /**
1007         * A callback method that is executed when the user session is invalidated
1008         * either by explicit call to {@link org.apache.wicket.Session#invalidate()}
1009         * or due to HttpSession expiration.
1010         *
1011         * <p>In case of session expiration this method is called in a non-worker thread, i.e.
1012         * there are no thread locals exported for the Application, RequestCycle and Session.
1013         * The Session is the current instance. The Application can be found by using
1014         * {@link Application#get(String)}. There is no way to get a reference to a RequestCycle</p>
1015         */
1016        public void onInvalidate()
1017        {
1018        }
1019        
1020        /**
1021         * Change the id of the underlying (Web)Session if this last one is permanent.
1022         * <p>
1023         * Call upon login to protect against session fixation.
1024         * 
1025         * @see "http://www.owasp.org/index.php/Session_Fixation"
1026         */
1027        public void changeSessionId()
1028        {
1029                if (isTemporary())
1030                {
1031                        return;
1032                }
1033                
1034                id = generateNewSessionId();
1035        }
1036
1037        /**
1038         * Change the id of the underlying (Web)Session.
1039         * 
1040         * @return the new session id value.
1041         */
1042        protected abstract String generateNewSessionId();
1043
1044        /**
1045         * Factory method for PageAccessSynchronizer instances
1046         *
1047         * @param timeout
1048         *              The configured timeout. See {@link org.apache.wicket.settings.RequestCycleSettings#getTimeout()}
1049         * @return A new instance of PageAccessSynchronizer
1050         */
1051        protected PageAccessSynchronizer newPageAccessSynchronizer(Duration timeout)
1052        {
1053                return new PageAccessSynchronizer(timeout);
1054        }
1055
1056        private final class PageAccessSynchronizerProvider extends LazyInitializer<PageAccessSynchronizer>
1057        {
1058                private static final long serialVersionUID = 1L;
1059
1060                @Override
1061                protected PageAccessSynchronizer createInstance()
1062                {
1063                        final Duration timeout;
1064                        if (Application.exists())
1065                        {
1066                                timeout = Application.get().getRequestCycleSettings().getTimeout();
1067                        }
1068                        else
1069                        {
1070                                timeout = Duration.ofMinutes(1);
1071                                log.warn(
1072                                        "PageAccessSynchronizer created outside of application thread, using default timeout: {}",
1073                                        timeout);
1074                        }
1075                        return newPageAccessSynchronizer(timeout);
1076                }
1077        }
1078
1079}