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