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.protocol.http;
018
019import java.io.UnsupportedEncodingException;
020import java.nio.charset.Charset;
021import java.time.Duration;
022import java.util.Collection;
023import java.util.LinkedList;
024import java.util.Locale;
025import java.util.function.Function;
026
027import javax.servlet.ServletContext;
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030import javax.servlet.http.HttpSession;
031
032import org.apache.wicket.Application;
033import org.apache.wicket.Page;
034import org.apache.wicket.RuntimeConfigurationType;
035import org.apache.wicket.Session;
036import org.apache.wicket.WicketRuntimeException;
037import org.apache.wicket.ajax.AjaxRequestHandler;
038import org.apache.wicket.ajax.AjaxRequestTarget;
039import org.apache.wicket.ajax.AjaxRequestTargetListenerCollection;
040import org.apache.wicket.coep.CrossOriginEmbedderPolicyConfiguration;
041import org.apache.wicket.coep.CrossOriginEmbedderPolicyRequestCycleListener;
042import org.apache.wicket.coop.CrossOriginOpenerPolicyConfiguration;
043import org.apache.wicket.coop.CrossOriginOpenerPolicyRequestCycleListener;
044import org.apache.wicket.core.request.mapper.IMapperContext;
045import org.apache.wicket.core.request.mapper.MountedMapper;
046import org.apache.wicket.core.request.mapper.PackageMapper;
047import org.apache.wicket.core.request.mapper.ResourceMapper;
048import org.apache.wicket.core.util.file.WebApplicationPath;
049import org.apache.wicket.core.util.resource.ClassPathResourceFinder;
050import org.apache.wicket.csp.CSPHeaderConfiguration;
051import org.apache.wicket.csp.ContentSecurityPolicySettings;
052import org.apache.wicket.markup.MarkupType;
053import org.apache.wicket.markup.head.CssHeaderItem;
054import org.apache.wicket.markup.head.JavaScriptHeaderItem;
055import org.apache.wicket.markup.html.WebPage;
056import org.apache.wicket.markup.html.form.AutoLabelResolver;
057import org.apache.wicket.markup.html.form.AutoLabelTextResolver;
058import org.apache.wicket.markup.html.pages.AccessDeniedPage;
059import org.apache.wicket.markup.html.pages.InternalErrorPage;
060import org.apache.wicket.markup.html.pages.PageExpiredErrorPage;
061import org.apache.wicket.markup.resolver.AutoLinkResolver;
062import org.apache.wicket.protocol.http.servlet.AbstractRequestWrapperFactory;
063import org.apache.wicket.protocol.http.servlet.FilterFactoryManager;
064import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
065import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
066import org.apache.wicket.request.IRequestHandler;
067import org.apache.wicket.request.IRequestMapper;
068import org.apache.wicket.request.Request;
069import org.apache.wicket.request.Response;
070import org.apache.wicket.request.Url;
071import org.apache.wicket.request.cycle.RequestCycle;
072import org.apache.wicket.request.handler.render.WebPageRenderer;
073import org.apache.wicket.request.http.WebRequest;
074import org.apache.wicket.request.http.WebResponse;
075import org.apache.wicket.request.mapper.ICompoundRequestMapper;
076import org.apache.wicket.request.mapper.IRequestMapperDelegate;
077import org.apache.wicket.request.resource.CssResourceReference;
078import org.apache.wicket.request.resource.JavaScriptResourceReference;
079import org.apache.wicket.request.resource.ResourceReference;
080import org.apache.wicket.resource.bundles.ReplacementResourceBundleReference;
081import org.apache.wicket.session.HttpSessionStore;
082import org.apache.wicket.util.crypt.CharEncoding;
083import org.apache.wicket.util.file.FileCleaner;
084import org.apache.wicket.util.file.IFileCleaner;
085import org.apache.wicket.util.file.Path;
086import org.apache.wicket.util.lang.Args;
087import org.apache.wicket.util.lang.PackageName;
088import org.apache.wicket.util.string.Strings;
089import org.apache.wicket.util.watch.IModificationWatcher;
090import org.slf4j.Logger;
091import org.slf4j.LoggerFactory;
092
093
094/**
095 * A web application is a subclass of Application which associates with an instance of WicketServlet
096 * to serve pages over the HTTP protocol. This class is intended to be subclassed by framework
097 * clients to define a web application.
098 * <p>
099 * Application settings are given defaults by the WebApplication() constructor and internalInit
100 * method, such as error page classes appropriate for HTML. WebApplication subclasses can override
101 * these values and/or modify other application settings by overriding the init() method and then by
102 * calling getXXXSettings() to retrieve an interface to a mutable Settings object. Do not do this in
103 * the constructor itself because the defaults will then override your settings.
104 * <p>
105 * If you want to use a filter specific configuration, e.g. using init parameters from the
106 * {@link javax.servlet.FilterConfig} object, you should override the init() method. For example:
107 * 
108 * <pre>
109 *  public void init() {
110 *  String webXMLParameter = getInitParameter(&quot;myWebXMLParameter&quot;);
111 *  URL schedulersConfig = getServletContext().getResource(&quot;/WEB-INF/schedulers.xml&quot;);
112 *  ...
113 * </pre>
114 * 
115 * @see WicketFilter
116 * @see org.apache.wicket.settings.ApplicationSettings
117 * @see org.apache.wicket.settings.DebugSettings
118 * @see org.apache.wicket.settings.ExceptionSettings
119 * @see org.apache.wicket.settings.MarkupSettings
120 * @see org.apache.wicket.settings.PageSettings
121 * @see org.apache.wicket.settings.RequestCycleSettings
122 * @see org.apache.wicket.settings.ResourceSettings
123 * @see org.apache.wicket.settings.SecuritySettings
124 * @see javax.servlet.Filter
125 * @see javax.servlet.FilterConfig
126 * @see javax.servlet.ServletContext
127 * 
128 * @author Jonathan Locke
129 * @author Chris Turner
130 * @author Johan Compagner
131 * @author Eelco Hillenius
132 * @author Juergen Donnerstag
133 */
134public abstract class WebApplication extends Application
135{
136        /** Log. */
137        private static final Logger log = LoggerFactory.getLogger(WebApplication.class);
138
139        public static final String META_INF_RESOURCES = "META-INF/resources";
140
141        private ServletContext servletContext;
142
143        private final AjaxRequestTargetListenerCollection ajaxRequestTargetListeners;
144
145        private Function<Page, AjaxRequestTarget> ajaxRequestTargetProvider;
146
147        private FilterFactoryManager filterFactoryManager;
148
149        /**
150         * Cached value of the parsed (from system properties or Servlet init/context parameter)
151         * <code>wicket.configuration</code> setting. No need to re-read it because it wont change at
152         * runtime.
153         */
154        private RuntimeConfigurationType configurationType;
155        
156        private ContentSecurityPolicySettings cspSettings;
157
158        /**
159         * Covariant override for easy getting the current {@link WebApplication} without having to cast
160         * it.
161         */
162        public static WebApplication get()
163        {
164                Application application = Application.get();
165
166                if (application instanceof WebApplication == false)
167                {
168                        throw new WicketRuntimeException(
169                                "The application attached to the current thread is not a " +
170                                        WebApplication.class.getSimpleName());
171                }
172
173                return (WebApplication)application;
174        }
175
176        /**
177         * the prefix for storing variables in the actual session (typically {@link HttpSession} for
178         * this application instance.
179         */
180        private String sessionAttributePrefix;
181
182        /** The WicketFilter that this application is attached to */
183        private WicketFilter wicketFilter;
184
185        /**
186         * Constructor. <strong>Use {@link #init()} for any configuration of your application instead of
187         * overriding the constructor.</strong>
188         */
189        public WebApplication()
190        {
191                ajaxRequestTargetListeners = new AjaxRequestTargetListenerCollection();
192        }
193
194        /**
195         * @see org.apache.wicket.Application#getApplicationKey()
196         */
197        @Override
198        public final String getApplicationKey()
199        {
200                return getName();
201        }
202
203        /**
204         * Gets an init parameter of the filter, or null if the parameter does not exist.
205         * 
206         * @param key
207         *            the key to search for
208         * @return the value of the filter init parameter
209         */
210        public String getInitParameter(String key)
211        {
212                if (wicketFilter != null)
213                {
214                        return wicketFilter.getFilterConfig().getInitParameter(key);
215                }
216                throw new IllegalStateException("init parameter '" + key +
217                        "' is not set yet. Any code in your" +
218                        " Application object that uses the wicketServlet/Filter instance should be put" +
219                        " in the init() method instead of your constructor");
220        }
221
222
223        /**
224         * Sets servlet context this application runs after. This is uaully done from a filter or a
225         * servlet responsible for managing this application object, such as {@link WicketFilter}
226         * 
227         * @param servletContext
228         */
229        public void setServletContext(ServletContext servletContext)
230        {
231                this.servletContext = servletContext;
232        }
233
234        /**
235         * Gets the servlet context for this application. Use this to get references to absolute paths,
236         * global web.xml parameters (&lt;context-param&gt;), etc.
237         * 
238         * @return The servlet context for this application
239         */
240        public ServletContext getServletContext()
241        {
242                if (servletContext == null)
243                {
244                        throw new IllegalStateException("servletContext is not set yet. Any code in your"
245                                + " Application object that uses the wicket filter instance should be put"
246                                + " in the init() method instead of your constructor");
247                }
248                return servletContext;
249        }
250
251        /**
252         * Gets the prefix for storing variables in the actual session (typically {@link HttpSession}
253         * for this application instance.
254         * 
255         * @param request
256         *            the request
257         * @param filterName
258         *            If null, than it defaults to the WicketFilter filter name. However according to
259         *            the ServletSpec the Filter is not guaranteed to be initialized e.g. when
260         *            WicketSessionFilter gets initialized. Thus, though you (and WicketSessionFilter)
261         *            can provide a filter name, you must make sure it is the same as WicketFilter will
262         *            provide once initialized.
263         * 
264         * @return the prefix for storing variables in the actual session
265         */
266        public String getSessionAttributePrefix(final WebRequest request, String filterName)
267        {
268                if (sessionAttributePrefix == null)
269                {
270                        if (filterName == null)
271                        {
272                                // According to the ServletSpec, the filter might not yet been initialized
273                                filterName = getWicketFilter().getFilterConfig().getFilterName();
274                        }
275                        String namespace = getMapperContext().getNamespace();
276                        sessionAttributePrefix = namespace + ':' + filterName + ':';
277                }
278
279                // Namespacing for session attributes is provided by
280                // adding the servlet path
281                return sessionAttributePrefix;
282        }
283
284        /**
285         * @return The Wicket filter for this application
286         */
287        public final WicketFilter getWicketFilter()
288        {
289                return wicketFilter;
290        }
291
292        /**
293         * @see org.apache.wicket.Application#logEventTarget(org.apache.wicket.request.IRequestHandler)
294         */
295        @Override
296        public void logEventTarget(IRequestHandler target)
297        {
298                super.logEventTarget(target);
299                IRequestLogger rl = getRequestLogger();
300                if (rl != null)
301                {
302                        rl.logEventTarget(target);
303                }
304        }
305
306        /**
307         * @see org.apache.wicket.Application#logResponseTarget(org.apache.wicket.request.IRequestHandler)
308         */
309        @Override
310        public void logResponseTarget(IRequestHandler target)
311        {
312                super.logResponseTarget(target);
313                IRequestLogger rl = getRequestLogger();
314                if (rl != null)
315                {
316                        rl.logResponseTarget(target);
317                }
318        }
319
320        /**
321         * Mounts an encoder at the given path.
322         * 
323         * @param mapper
324         *            the encoder that will be used for this mount
325         */
326        public void mount(final IRequestMapper mapper)
327        {
328                Args.notNull(mapper, "mapper");
329                getRootRequestMapperAsCompound().add(mapper);
330        }
331
332        /**
333         * Mounts a page class to the given path.
334         * 
335         * <p>
336         * NOTE: mount path must not start with reserved URL segments! See {@link IMapperContext} to know
337         * which segments are reserved for internal use.
338         * </p>
339         * @param <T>
340         *            type of page
341         * 
342         * @param path
343         *            the path to mount the page class on
344         * @param pageClass
345         *            the page class to be mounted
346         */
347        public <T extends Page> MountedMapper mountPage(final String path, final Class<T> pageClass)
348        {
349                MountedMapper mapper = new MountedMapper(path, pageClass);
350                mount(mapper);
351                return mapper;
352        }
353
354        /**
355         * Mounts a shared resource to the given path.
356         * 
357         * <p>
358         * NOTE: mount path must not start with reserved URL segments! See {@link IMapperContext} to know
359         * which segments are reserved for internal use.
360         * </p>
361         * @param path
362         *            the path to mount the resource reference on
363         * @param reference
364         *            resource reference to be mounted
365         */
366        public ResourceMapper mountResource(final String path, final ResourceReference reference)
367        {
368                if (reference.canBeRegistered())
369                {
370                        getResourceReferenceRegistry().registerResourceReference(reference);
371                }
372                ResourceMapper mapper = new ResourceMapper(path, reference);
373                mount(mapper);
374                return mapper;
375        }
376
377        /**
378         * Mounts mounts all bookmarkable pages in a the pageClass's package to the given path.
379         *
380         * <p>
381         * NOTE: mount path must not start with reserved URL segments! See {@link IMapperContext} to know
382         * which segments are reserved for internal use.
383         * </p>
384         * @param <P>
385         *            type of page
386         * 
387         * @param path
388         *            the path to mount the page class on
389         * @param pageClass
390         *            the page class to be mounted
391         */
392        public <P extends Page> PackageMapper mountPackage(final String path, final Class<P> pageClass)
393        {
394                PackageMapper packageMapper = new PackageMapper(path, PackageName.forClass(pageClass));
395                mount(packageMapper);
396                return packageMapper;
397        }
398
399        /**
400         * Unregisters all {@link IRequestMapper}s which would match on a this path.
401         * <p>
402         * Useful in OSGi environments where a bundle may want to update the mount point.
403         * </p>
404         * 
405         * @param path
406         *            the path to unmount
407         */
408        public void unmount(String path)
409        {
410                Args.notNull(path, "path");
411
412                if (path.charAt(0) == '/')
413                {
414                        path = path.substring(1);
415                }
416
417                IRequestMapper mapper = getRootRequestMapper();
418
419                while (mapper instanceof IRequestMapperDelegate)
420                {
421                        mapper = ((IRequestMapperDelegate) mapper).getDelegateMapper();
422                }
423
424                /*
425                 * Only attempt to unmount if root request mapper is either a compound, or wraps a compound to avoid leaving the
426                 * application with no mappers installed.
427                 */
428                if (mapper instanceof ICompoundRequestMapper)
429                {
430                        final Url url = Url.parse(path);
431
432                        Request request = new Request()
433                        {
434                                @Override
435                                public Url getUrl()
436                                {
437                                        return url;
438                                }
439
440                                @Override
441                                public Url getClientUrl()
442                                {
443                                        return url;
444                                }
445
446                                @Override
447                                public Locale getLocale()
448                                {
449                                        return null;
450                                }
451
452                                @Override
453                                public Charset getCharset()
454                                {
455                                        return null;
456                                }
457
458                                @Override
459                                public Object getContainerRequest()
460                                {
461                                        return null;
462                                }
463                        };
464
465                        unmountFromCompound((ICompoundRequestMapper) mapper, request);
466                }
467        }
468
469        /**
470         * Descends the tree of {@link ICompoundRequestMapper}s and {@link IRequestMapperDelegate}s to find the correct descendant
471         * to remove.
472         *
473         * @param parent
474         *              The {@link ICompoundRequestMapper} from which to unmount the matching mapper.
475         * @param request
476         *              The request used to find the mapper to remove.
477         */
478        private void unmountFromCompound(ICompoundRequestMapper parent, Request request)
479        {
480                Collection<IRequestMapper> toRemove = new LinkedList<>();
481
482                for (IRequestMapper mapper : parent)
483                {
484                        if (mapper.mapRequest(request) != null)
485                        {
486                                IRequestMapper actualMapper = mapper;
487
488                                while (actualMapper instanceof IRequestMapperDelegate)
489                                {
490                                        actualMapper = ((IRequestMapperDelegate) actualMapper).getDelegateMapper();
491                                }
492
493                                if (actualMapper instanceof ICompoundRequestMapper)
494                                {
495                                        unmountFromCompound((ICompoundRequestMapper) actualMapper, request);
496                                }
497                                else
498                                {
499                                        toRemove.add(mapper);
500                                }
501                        }
502                }
503
504                for (IRequestMapper mapper : toRemove)
505                {
506                        parent.remove(mapper);
507                }
508        }
509
510        /**
511         * Registers a replacement resource for the given javascript resource. This replacement can be
512         * another {@link JavaScriptResourceReference} for a packaged resource, but it can also be an
513         * {@link org.apache.wicket.request.resource.UrlResourceReference} to replace the resource by a
514         * resource hosted on a CDN. Registering a replacement will cause the resource to replaced by
515         * the given resource throughout the application: if {@code base} is added, {@code replacement}
516         * will be added instead.
517         * 
518         * @param base
519         *            The resource to replace
520         * @param replacement
521         *            The replacement
522         */
523        public final void addResourceReplacement(JavaScriptResourceReference base,
524                ResourceReference replacement)
525        {
526                ReplacementResourceBundleReference bundle = new ReplacementResourceBundleReference(replacement);
527                bundle.addProvidedResources(JavaScriptHeaderItem.forReference(base));
528                getResourceBundles().addBundle(JavaScriptHeaderItem.forReference(bundle));
529        }
530
531        /**
532         * Registers a replacement resource for the given CSS resource. This replacement can be another
533         * {@link CssResourceReference} for a packaged resource, but it can also be an
534         * {@link org.apache.wicket.request.resource.UrlResourceReference} to replace the resource by a
535         * resource hosted on a CDN. Registering a replacement will cause the resource to replaced by
536         * the given resource throughout the application: if {@code base} is added, {@code replacement}
537         * will be added instead.
538         * 
539         * @param base
540         *            The resource to replace
541         * @param replacement
542         *            The replacement
543         */
544        public final void addResourceReplacement(CssResourceReference base,
545                ResourceReference replacement)
546        {
547                ReplacementResourceBundleReference bundle = new ReplacementResourceBundleReference(replacement);
548                bundle.addProvidedResources(CssHeaderItem.forReference(base));
549                getResourceBundles().addBundle(CssHeaderItem.forReference(bundle));
550        }
551
552        /**
553         * Create a new WebRequest. Subclasses of WebRequest could e.g. decode and obfuscate URL which
554         * has been encoded by an appropriate WebResponse.
555         * 
556         * @param servletRequest
557         *            the current HTTP Servlet request
558         * @param filterPath
559         *            the filter mapping read from web.xml
560         * @return a WebRequest object
561         */
562        public WebRequest newWebRequest(HttpServletRequest servletRequest, final String filterPath)
563        {
564                return new ServletWebRequest(servletRequest, filterPath);
565        }
566
567        /**
568         * Pre- and post- configures the {@link WebRequest} created by user override-able
569         * {@link #newWebRequest(HttpServletRequest, String)}
570         * 
571         * @param servletRequest
572         *            the current HTTP Servlet request
573         * @param filterPath
574         *            the filter mapping read from web.xml
575         * @return a WebRequest object
576         */
577        WebRequest createWebRequest(HttpServletRequest servletRequest, final String filterPath)
578        {
579                if (hasFilterFactoryManager())
580                {
581                        for (AbstractRequestWrapperFactory factory : getFilterFactoryManager())
582                        {
583                                servletRequest = factory.getWrapper(servletRequest);
584                        }
585                }
586
587                WebRequest webRequest = newWebRequest(servletRequest, filterPath);
588
589                if (servletRequest.getCharacterEncoding() == null)
590                {
591                        try
592                        {
593                                if (webRequest.isAjax())
594                                {
595                                        // WICKET-3908, WICKET-1816: Forms submitted with Ajax are always UTF-8 encoded
596                                        servletRequest.setCharacterEncoding(CharEncoding.UTF_8);
597                                }
598                                else
599                                {
600                                        String requestEncoding = getRequestCycleSettings().getResponseRequestEncoding();
601                                        servletRequest.setCharacterEncoding(requestEncoding);
602                                }
603                        }
604                        catch (UnsupportedEncodingException e)
605                        {
606                                throw new WicketRuntimeException(e);
607                        }
608                }
609
610                return webRequest;
611        }
612
613        /**
614         * Creates a WebResponse. Subclasses of WebRequest could e.g. encode wicket's default URL and
615         * hide the details from the user. A appropriate WebRequest must be implemented and configured
616         * to decode the encoded URL.
617         * 
618         * @param webRequest
619         *            the {@link WebRequest} that will handle the current HTTP Servlet request
620         * @param httpServletResponse
621         *            the current HTTP Servlet response
622         * @return a WebResponse object
623         */
624        protected WebResponse newWebResponse(final WebRequest webRequest,
625                final HttpServletResponse httpServletResponse)
626        {
627                return new ServletWebResponse((ServletWebRequest)webRequest, httpServletResponse);
628        }
629
630        /**
631         * Pre- and post- configures the {@link WebResponse} returned from
632         * {@link #newWebResponse(WebRequest, HttpServletResponse)}
633         * 
634         * @param webRequest
635         *            the {@link WebRequest} that will handle the current HTTP Servlet request
636         * @param httpServletResponse
637         *            the current HTTP Servlet response
638         * @return the configured WebResponse object
639         */
640        WebResponse createWebResponse(final WebRequest webRequest,
641                final HttpServletResponse httpServletResponse)
642        {
643                WebResponse webResponse = newWebResponse(webRequest, httpServletResponse);
644
645                boolean shouldBufferResponse = getRequestCycleSettings().getBufferResponse();
646                return shouldBufferResponse ? new HeaderBufferingWebResponse(webResponse) : webResponse;
647        }
648
649        /**
650         * @see org.apache.wicket.Application#newSession(org.apache.wicket.request.Request,
651         *      org.apache.wicket.request.Response)
652         */
653        @Override
654        public Session newSession(Request request, Response response)
655        {
656                return new WebSession(request);
657        }
658
659        /**
660         * @see org.apache.wicket.Application#sessionUnbound(java.lang.String)
661         */
662        @Override
663        public void sessionUnbound(final String sessionId)
664        {
665                super.sessionUnbound(sessionId);
666
667                IRequestLogger logger = getRequestLogger();
668                if (logger != null)
669                {
670                        logger.sessionDestroyed(sessionId);
671                }
672        }
673
674        /**
675         * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
676         * 
677         * @param wicketFilter
678         *            The wicket filter instance for this application
679         */
680        public final void setWicketFilter(final WicketFilter wicketFilter)
681        {
682                Args.notNull(wicketFilter, "wicketFilter");
683                this.wicketFilter = wicketFilter;
684                servletContext = wicketFilter.getFilterConfig().getServletContext();
685        }
686
687        /**
688         * Initialize; if you need the wicket servlet/filter for initialization, e.g. because you want
689         * to read an initParameter from web.xml or you want to read a resource from the servlet's
690         * context path, you can override this method and provide custom initialization. This method is
691         * called right after this application class is constructed, and the wicket servlet/filter is
692         * set. <strong>Use this method for any application setup instead of the constructor.</strong>
693         */
694        @Override
695        protected void init()
696        {
697                super.init();
698        }
699
700        /**
701         * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
702         */
703        @Override
704        public void internalDestroy()
705        {
706                // destroy the resource watcher
707                IModificationWatcher resourceWatcher = getResourceSettings().getResourceWatcher(false);
708                if (resourceWatcher != null)
709                {
710                        resourceWatcher.destroy();
711                }
712
713                IFileCleaner fileCleaner = getResourceSettings().getFileCleaner();
714                if (fileCleaner != null)
715                {
716                        fileCleaner.destroy();
717                }
718
719                super.internalDestroy();
720        }
721
722        /**
723         * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
724         * 
725         * Internal initialization. First determine the deployment mode. First check the system property
726         * -Dwicket.configuration. If it does not exist check the servlet init parameter (
727         * <code>&lt;init-param&gt;&lt;param-name&gt;configuration&lt;/param-name&gt;</code>). If not
728         * found check the servlet context init parameter
729         * <code>&lt;context-param&gt;&lt;param-name6gt;configuration&lt;/param-name&gt;</code>). If the
730         * parameter is "development" (which is default), settings appropriate for development are set.
731         * If it's "deployment" , deployment settings are used. If development is specified and a
732         * "sourceFolder" init parameter is also set, then resources in that folder will be polled for
733         * changes.
734         */
735        @Override
736        protected void internalInit()
737        {
738                super.internalInit();
739
740                getResourceSettings().getResourceFinders().add(
741                        new WebApplicationPath(getServletContext(), ""));
742                getResourceSettings().getResourceFinders().add(
743                        new ClassPathResourceFinder(META_INF_RESOURCES));
744
745                // Set default error pages for HTML markup
746                getApplicationSettings().setPageExpiredErrorPage(PageExpiredErrorPage.class);
747                getApplicationSettings().setInternalErrorPage(InternalErrorPage.class);
748                getApplicationSettings().setAccessDeniedPage(AccessDeniedPage.class);
749
750                // Add resolver for automatically resolving HTML links
751                getPageSettings().addComponentResolver(new AutoLinkResolver());
752                getPageSettings().addComponentResolver(new AutoLabelResolver());
753                getPageSettings().addComponentResolver(new AutoLabelTextResolver());
754
755                getResourceSettings().setFileCleaner(new FileCleaner());
756
757                setPageRendererProvider(WebPageRenderer::new);
758                setSessionStoreProvider(HttpSessionStore::new);
759                setAjaxRequestTargetProvider(AjaxRequestHandler::new);
760
761                getAjaxRequestTargetListeners().add(new AjaxEnclosureListener());
762                
763                // Configure the app.
764                configure();
765                if (getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT)
766                {
767                        // Add optional sourceFolder for resources.
768                        String resourceFolder = getInitParameter("sourceFolder");
769                        if (resourceFolder != null)
770                        {
771                                getResourceSettings().getResourceFinders().add(new Path(resourceFolder));
772                        }
773                        getCspSettings().blocking().reportBack();
774                }
775                getCspSettings().blocking().strict();
776        }
777
778        @Override
779        protected void validateInit()
780        {
781                super.validateInit();
782
783                if (getCspSettings().isEnabled()) {
784                        getCspSettings().enforce(this);
785                }
786
787                // enable coop and coep listeners if specified in security settings
788                CrossOriginOpenerPolicyConfiguration coopConfig = getSecuritySettings()
789                        .getCrossOriginOpenerPolicyConfiguration();
790                if (coopConfig.isEnabled())
791                {
792                        getRequestCycleListeners()
793                                .add(new CrossOriginOpenerPolicyRequestCycleListener(coopConfig));
794                }
795
796                CrossOriginEmbedderPolicyConfiguration coepConfig = getSecuritySettings()
797                        .getCrossOriginEmbedderPolicyConfiguration();
798                if (coepConfig.isEnabled())
799                {
800                        getRequestCycleListeners()
801                                .add(new CrossOriginEmbedderPolicyRequestCycleListener(coepConfig));
802                }
803        }
804
805        /**
806         * set runtime configuration type
807         * <p/>
808         * this is a write-once property: once configured it can not be changed later on.
809         * 
810         * @param configurationType
811         */
812        public Application setConfigurationType(RuntimeConfigurationType configurationType)
813        {
814                if (this.configurationType != null)
815                {
816                        throw new IllegalStateException(
817                                "Configuration type is write-once. You can not change it. " + "" +
818                                        "Current value='" + configurationType + "'");
819                }
820                this.configurationType = Args.notNull(configurationType, "configurationType");
821                return this;
822        }
823
824        /**
825         * {@inheritDoc}
826         */
827        @Override
828        public RuntimeConfigurationType getConfigurationType()
829        {
830                if (configurationType == null)
831                {
832                        String result = null;
833                        try
834                        {
835                                result = System.getProperty("wicket." + Application.CONFIGURATION);
836                        }
837                        catch (SecurityException e)
838                        {
839                                log.warn("SecurityManager doesn't allow to read the configuration type from " +
840                                                "the system properties. The configuration type will be read from the web.xml.");
841                        }
842
843                        // If no system parameter check filter/servlet <init-param> and <context-param>
844                        if (result == null)
845                        {
846                                result = getInitParameter("wicket." + Application.CONFIGURATION);
847                        }
848                        if (result == null)
849                        {
850                                result = getServletContext().getInitParameter("wicket." + Application.CONFIGURATION);
851                        }
852
853                        // If no system parameter check filter/servlet specific <init-param>
854                        if (result == null)
855                        {
856                                result = getInitParameter(Application.CONFIGURATION);
857                        }
858
859                        // If no system parameter and no <init-param>, then check
860                        // <context-param>
861                        if (result == null)
862                        {
863                                result = getServletContext().getInitParameter(Application.CONFIGURATION);
864                        }
865
866                        // Return result if we have found it, else fall back to DEVELOPMENT mode
867                        // as the default.
868                        if (result != null)
869                        {
870                                try
871                                {
872                                        configurationType = RuntimeConfigurationType.valueOf(result.toUpperCase(Locale.ROOT));
873                                }
874                                catch (IllegalArgumentException e)
875                                {
876                                        // Ignore : fall back to DEVELOPMENT mode
877                                        // log.warn("Unknown runtime configuration type '" + result +
878                                        // "', falling back to DEVELOPMENT mode.");
879                                        throw new IllegalArgumentException("Invalid configuration type: '" + result +
880                                                "'.  Must be \"development\" or \"deployment\".");
881                                }
882                        }
883                }
884
885                if (configurationType == null)
886                {
887                        configurationType = RuntimeConfigurationType.DEVELOPMENT;
888                }
889
890                return configurationType;
891        }
892
893        /**
894         * The rules if and when to insert an xml decl in the response are a bit tricky. Hence, we allow
895         * the user to replace the default implementation per page and per application.
896         * <p>
897         * Default implementation: the page mime type must be "application/xhtml+xml" and request
898         * HTTP_ACCEPT header must include "application/xhtml+xml" to automatically include the xml
899         * decl. Please see <a href=
900         * "https://developer.mozilla.org/en/Writing_JavaScript_for_XHTML#Finally:_Content_Negotiation"
901         * >Writing JavaScript for XHTML</a> for details.
902         * <p>
903         * Please note that xml decls in Wicket's markup are only used for reading the markup. The
904         * markup's xml decl will always be removed and never be used to configure the response.
905         * 
906         * @param page
907         *            The page currently being rendered
908         * @param insert
909         *            If false, than the rules are applied. If true, it'll always be written. In order
910         *            to never insert it, than subclass renderXmlDecl() with an empty implementation.
911         */
912        public void renderXmlDecl(final WebPage page, boolean insert)
913        {
914                if (insert || MarkupType.XML_MIME.equalsIgnoreCase(page.getMarkupType().getMimeType()))
915                {
916                        final RequestCycle cycle = RequestCycle.get();
917
918                        if (insert == false)
919                        {
920                                WebRequest request = (WebRequest)cycle.getRequest();
921
922                                String accept = request.getHeader("Accept");
923                                insert = ((accept == null) || (accept.indexOf(MarkupType.XML_MIME) != -1));
924                        }
925
926                        if (insert)
927                        {
928                                WebResponse response = (WebResponse)cycle.getResponse();
929                                response.write("<?xml version='1.0'");
930                                String encoding = getRequestCycleSettings().getResponseRequestEncoding();
931                                if (Strings.isEmpty(encoding) == false)
932                                {
933                                        response.write(" encoding='");
934                                        response.write(encoding);
935                                        response.write("'");
936                                }
937                                response.write(" ?>");
938                        }
939                }
940        }
941
942        /**
943         * Creates a new ajax request target used to control ajax responses
944         * 
945         * @param page
946         *            page on which ajax response is made
947         * @return non-null ajax request target instance
948         */
949        public final AjaxRequestTarget newAjaxRequestTarget(final Page page)
950        {
951                AjaxRequestTarget target = getAjaxRequestTargetProvider().apply(page);
952                for (AjaxRequestTarget.IListener listener : ajaxRequestTargetListeners)
953                {
954                        target.addListener(listener);
955                }
956                return target;
957        }
958
959        /**
960         * Log that this application is started.
961         */
962        final void logStarted()
963        {
964                if (log.isInfoEnabled())
965                {
966                        String version = getFrameworkSettings().getVersion();
967                        StringBuilder b = new StringBuilder();
968                        b.append("[").append(getName()).append("] Started Wicket ");
969                        if (!"n/a".equals(version))
970                        {
971                                b.append("version ").append(version).append(" ");
972                        }
973                        b.append("in ").append(getConfigurationType()).append(" mode");
974                        log.info(b.toString());
975                }
976
977                if (usesDevelopmentConfig())
978                {
979                        outputDevelopmentModeWarning();
980                }
981        }
982
983        /**
984         * This method prints a warning to stderr that we are starting in development mode.
985         * <p>
986         * If you really need to test Wicket in development mode on a staging server somewhere and are
987         * annoying the sysadmin for it with stderr messages, you can override this to make it do
988         * something else.
989         */
990        protected void outputDevelopmentModeWarning()
991        {
992                System.err.print("********************************************************************\n"
993                        + "*** WARNING: Wicket is running in DEVELOPMENT mode.              ***\n"
994                        + "***                               ^^^^^^^^^^^                    ***\n"
995                        + "*** Do NOT deploy to your live server(s) without changing this.  ***\n"
996                        + "*** See Application#getConfigurationType() for more information. ***\n"
997                        + "********************************************************************\n");
998        }
999
1000        /*
1001         * Can contain at most 1000 responses and each entry can live at most one minute. For now there
1002         * is no need to configure these parameters externally.
1003         */
1004        private final StoredResponsesMap storedResponses = new StoredResponsesMap(1000,
1005                Duration.ofSeconds(60));
1006
1007        /**
1008         * 
1009         * @param sessionId
1010         * @param url
1011         * @return true if has buffered response
1012         */
1013        public boolean hasBufferedResponse(String sessionId, Url url)
1014        {
1015                String key = sessionId + url.toString();
1016                return storedResponses.containsKey(key);
1017        }
1018
1019        /**
1020         * Retrieves a stored buffered response for a given sessionId and url.
1021         *
1022         * @param url
1023         *          The url used as a key
1024         * @return the stored buffered response. {@code null} if there is no stored response for the given url
1025         * @see org.apache.wicket.settings.RequestCycleSettings.RenderStrategy#REDIRECT_TO_BUFFER
1026         */
1027        public BufferedWebResponse getAndRemoveBufferedResponse(String sessionId, Url url)
1028        {
1029                String key = sessionId + url.toString();
1030                return storedResponses.remove(key);
1031        }
1032
1033        /**
1034         * Store the buffered response at application level to use it at a later time.
1035         *
1036         * @param sessionId
1037         * @param url
1038         * @param response
1039         */
1040        public void storeBufferedResponse(String sessionId, Url url, BufferedWebResponse response)
1041        {
1042                if (Strings.isEmpty(sessionId))
1043                {
1044                        log.error("storeBufferedResponse needs a valid session id to store the response, but a null one was found. "
1045                                        + "Please report the problem to dev team and try to reproduce it in a quickstart project.");
1046                        return;
1047                }
1048
1049                String key = sessionId + url.toString();
1050                storedResponses.put(key, response);
1051        }
1052
1053        @Override
1054        public String getMimeType(String fileName)
1055        {
1056                String mimeType = getServletContext().getMimeType(fileName);
1057                return mimeType != null ? mimeType : super.getMimeType(fileName);
1058        }
1059
1060        /**
1061         * Returns the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1062         * 
1063         * @return the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1064         */
1065        public Function<Page, AjaxRequestTarget> getAjaxRequestTargetProvider()
1066        {
1067                return ajaxRequestTargetProvider;
1068        }
1069
1070        /**
1071         * Sets the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1072         * 
1073         * @param ajaxRequestTargetProvider
1074         *            the new provider
1075         */
1076        public Application setAjaxRequestTargetProvider(
1077                Function<Page, AjaxRequestTarget> ajaxRequestTargetProvider)
1078        {
1079                this.ajaxRequestTargetProvider = ajaxRequestTargetProvider;
1080                return this;
1081        }
1082
1083        /**
1084         * Returns the registered {@link org.apache.wicket.ajax.AjaxRequestTarget.IListener} objects.
1085         * 
1086         * @return the registered {@link org.apache.wicket.ajax.AjaxRequestTarget.IListener} objects.
1087         */
1088        public AjaxRequestTargetListenerCollection getAjaxRequestTargetListeners()
1089        {
1090                return ajaxRequestTargetListeners;
1091        }
1092
1093        /**
1094         * @return True if at least one filter factory has been added.
1095         */
1096        public final boolean hasFilterFactoryManager()
1097        {
1098                return filterFactoryManager != null;
1099        }
1100
1101        /**
1102         * @return The filter factory manager
1103         */
1104        public final FilterFactoryManager getFilterFactoryManager()
1105        {
1106                if (filterFactoryManager == null)
1107                {
1108                        filterFactoryManager = new FilterFactoryManager();
1109                }
1110                return filterFactoryManager;
1111        }
1112        
1113        /**
1114         * TODO remove in Wicket 10
1115         * 
1116         * @deprecated use {@link #setCspSettings(ContentSecurityPolicySettings)} instead
1117         */
1118        protected ContentSecurityPolicySettings newCspSettings()
1119        {
1120                return new ContentSecurityPolicySettings(this);
1121        }
1122
1123        /**
1124         * Returns the {@link ContentSecurityPolicySettings} for this application. See
1125         * {@link ContentSecurityPolicySettings} and {@link CSPHeaderConfiguration} for instructions on
1126         * configuring the CSP for your specific needs.
1127         * 
1128         * @return The {@link ContentSecurityPolicySettings} for this application.
1129         * @see ContentSecurityPolicySettings
1130         * @see CSPHeaderConfiguration
1131         * 
1132         * TODO make final in Wicket 10
1133         */
1134        public ContentSecurityPolicySettings getCspSettings()
1135        {
1136                checkSettingsAvailable();
1137
1138                if (cspSettings == null)
1139                {
1140                        cspSettings = newCspSettings();
1141                }
1142                return cspSettings;
1143        }
1144
1145        /**
1146         * Set CSP settings.
1147         * 
1148         */
1149        public void setCspSettings(ContentSecurityPolicySettings cspSettings)
1150        {
1151                this.cspSettings = cspSettings;
1152        }
1153}