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