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