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}