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}