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                AjaxRequestTargetListenerCollection ajaxRequestTargetListeners = getAjaxRequestTargetListeners();
762                ajaxRequestTargetListeners.add(new AjaxEnclosureListener());
763                ajaxRequestTargetListeners.add(new MultipartFormComponentListener());
764
765                // Configure the app.
766                configure();
767                if (getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT)
768                {
769                        // Add optional sourceFolder for resources.
770                        String resourceFolder = getInitParameter("sourceFolder");
771                        if (resourceFolder != null)
772                        {
773                                getResourceSettings().getResourceFinders().add(new Path(resourceFolder));
774                        }
775                        getCspSettings().blocking().reportBack();
776                }
777                getCspSettings().blocking().strict();
778        }
779
780        @Override
781        protected void validateInit()
782        {
783                super.validateInit();
784
785                if (getCspSettings().isEnabled()) {
786                        getCspSettings().enforce(this);
787                }
788
789                // enable coop and coep listeners if specified in security settings
790                CrossOriginOpenerPolicyConfiguration coopConfig = getSecuritySettings()
791                        .getCrossOriginOpenerPolicyConfiguration();
792                if (coopConfig.isEnabled())
793                {
794                        getRequestCycleListeners()
795                                .add(new CrossOriginOpenerPolicyRequestCycleListener(coopConfig));
796                }
797
798                CrossOriginEmbedderPolicyConfiguration coepConfig = getSecuritySettings()
799                        .getCrossOriginEmbedderPolicyConfiguration();
800                if (coepConfig.isEnabled())
801                {
802                        getRequestCycleListeners()
803                                .add(new CrossOriginEmbedderPolicyRequestCycleListener(coepConfig));
804                }
805        }
806
807        /**
808         * set runtime configuration type
809         * <p/>
810         * this is a write-once property: once configured it can not be changed later on.
811         * 
812         * @param configurationType
813         */
814        public Application setConfigurationType(RuntimeConfigurationType configurationType)
815        {
816                if (this.configurationType != null)
817                {
818                        throw new IllegalStateException(
819                                "Configuration type is write-once. You can not change it. " + "" +
820                                        "Current value='" + configurationType + "'");
821                }
822                this.configurationType = Args.notNull(configurationType, "configurationType");
823                return this;
824        }
825
826        /**
827         * {@inheritDoc}
828         */
829        @Override
830        public RuntimeConfigurationType getConfigurationType()
831        {
832                if (configurationType == null)
833                {
834                        String result = null;
835                        try
836                        {
837                                result = System.getProperty("wicket." + Application.CONFIGURATION);
838                        }
839                        catch (SecurityException e)
840                        {
841                                log.warn("SecurityManager doesn't allow to read the configuration type from " +
842                                                "the system properties. The configuration type will be read from the web.xml.");
843                        }
844
845                        // If no system parameter check filter/servlet <init-param> and <context-param>
846                        if (result == null)
847                        {
848                                result = getInitParameter("wicket." + Application.CONFIGURATION);
849                        }
850                        if (result == null)
851                        {
852                                result = getServletContext().getInitParameter("wicket." + Application.CONFIGURATION);
853                        }
854
855                        // If no system parameter check filter/servlet specific <init-param>
856                        if (result == null)
857                        {
858                                result = getInitParameter(Application.CONFIGURATION);
859                        }
860
861                        // If no system parameter and no <init-param>, then check
862                        // <context-param>
863                        if (result == null)
864                        {
865                                result = getServletContext().getInitParameter(Application.CONFIGURATION);
866                        }
867
868                        // Return result if we have found it, else fall back to DEVELOPMENT mode
869                        // as the default.
870                        if (result != null)
871                        {
872                                try
873                                {
874                                        configurationType = RuntimeConfigurationType.valueOf(result.toUpperCase(Locale.ROOT));
875                                }
876                                catch (IllegalArgumentException e)
877                                {
878                                        // Ignore : fall back to DEVELOPMENT mode
879                                        // log.warn("Unknown runtime configuration type '" + result +
880                                        // "', falling back to DEVELOPMENT mode.");
881                                        throw new IllegalArgumentException("Invalid configuration type: '" + result +
882                                                "'.  Must be \"development\" or \"deployment\".");
883                                }
884                        }
885                }
886
887                if (configurationType == null)
888                {
889                        configurationType = RuntimeConfigurationType.DEVELOPMENT;
890                }
891
892                return configurationType;
893        }
894
895        /**
896         * The rules if and when to insert an xml decl in the response are a bit tricky. Hence, we allow
897         * the user to replace the default implementation per page and per application.
898         * <p>
899         * Default implementation: the page mime type must be "application/xhtml+xml" and request
900         * HTTP_ACCEPT header must include "application/xhtml+xml" to automatically include the xml
901         * decl. Please see <a href=
902         * "https://developer.mozilla.org/en/Writing_JavaScript_for_XHTML#Finally:_Content_Negotiation"
903         * >Writing JavaScript for XHTML</a> for details.
904         * <p>
905         * Please note that xml decls in Wicket's markup are only used for reading the markup. The
906         * markup's xml decl will always be removed and never be used to configure the response.
907         * 
908         * @param page
909         *            The page currently being rendered
910         * @param insert
911         *            If false, than the rules are applied. If true, it'll always be written. In order
912         *            to never insert it, than subclass renderXmlDecl() with an empty implementation.
913         */
914        public void renderXmlDecl(final WebPage page, boolean insert)
915        {
916                if (insert || MarkupType.XML_MIME.equalsIgnoreCase(page.getMarkupType().getMimeType()))
917                {
918                        final RequestCycle cycle = RequestCycle.get();
919
920                        if (insert == false)
921                        {
922                                WebRequest request = (WebRequest)cycle.getRequest();
923
924                                String accept = request.getHeader("Accept");
925                                insert = ((accept == null) || (accept.indexOf(MarkupType.XML_MIME) != -1));
926                        }
927
928                        if (insert)
929                        {
930                                WebResponse response = (WebResponse)cycle.getResponse();
931                                response.write("<?xml version='1.0'");
932                                String encoding = getRequestCycleSettings().getResponseRequestEncoding();
933                                if (Strings.isEmpty(encoding) == false)
934                                {
935                                        response.write(" encoding='");
936                                        response.write(encoding);
937                                        response.write("'");
938                                }
939                                response.write(" ?>");
940                        }
941                }
942        }
943
944        /**
945         * Creates a new ajax request target used to control ajax responses
946         * 
947         * @param page
948         *            page on which ajax response is made
949         * @return non-null ajax request target instance
950         */
951        public final AjaxRequestTarget newAjaxRequestTarget(final Page page)
952        {
953                AjaxRequestTarget target = getAjaxRequestTargetProvider().apply(page);
954                for (AjaxRequestTarget.IListener listener : ajaxRequestTargetListeners)
955                {
956                        target.addListener(listener);
957                }
958                return target;
959        }
960
961        /**
962         * Log that this application is started.
963         */
964        final void logStarted()
965        {
966                if (log.isInfoEnabled())
967                {
968                        String version = getFrameworkSettings().getVersion();
969                        StringBuilder b = new StringBuilder();
970                        b.append("[").append(getName()).append("] Started Wicket ");
971                        if (!"n/a".equals(version))
972                        {
973                                b.append("version ").append(version).append(" ");
974                        }
975                        b.append("in ").append(getConfigurationType()).append(" mode");
976                        log.info(b.toString());
977                }
978
979                if (usesDevelopmentConfig())
980                {
981                        outputDevelopmentModeWarning();
982                }
983        }
984
985        /**
986         * This method prints a warning to stderr that we are starting in development mode.
987         * <p>
988         * If you really need to test Wicket in development mode on a staging server somewhere and are
989         * annoying the sysadmin for it with stderr messages, you can override this to make it do
990         * something else.
991         */
992        protected void outputDevelopmentModeWarning()
993        {
994                System.err.print("********************************************************************\n"
995                        + "*** WARNING: Wicket is running in DEVELOPMENT mode.              ***\n"
996                        + "***                               ^^^^^^^^^^^                    ***\n"
997                        + "*** Do NOT deploy to your live server(s) without changing this.  ***\n"
998                        + "*** See Application#getConfigurationType() for more information. ***\n"
999                        + "********************************************************************\n");
1000        }
1001
1002        /*
1003         * Can contain at most 1000 responses and each entry can live at most one minute. For now there
1004         * is no need to configure these parameters externally.
1005         */
1006        private final StoredResponsesMap storedResponses = new StoredResponsesMap(1000,
1007                Duration.ofSeconds(60));
1008
1009        /**
1010         * 
1011         * @param sessionId
1012         * @param url
1013         * @return true if has buffered response
1014         */
1015        public boolean hasBufferedResponse(String sessionId, Url url)
1016        {
1017                String key = sessionId + url.toString();
1018                return storedResponses.containsKey(key);
1019        }
1020
1021        /**
1022         * Retrieves a stored buffered response for a given sessionId and url.
1023         *
1024         * @param url
1025         *          The url used as a key
1026         * @return the stored buffered response. {@code null} if there is no stored response for the given url
1027         * @see org.apache.wicket.settings.RequestCycleSettings.RenderStrategy#REDIRECT_TO_BUFFER
1028         */
1029        public BufferedWebResponse getAndRemoveBufferedResponse(String sessionId, Url url)
1030        {
1031                String key = sessionId + url.toString();
1032                return storedResponses.remove(key);
1033        }
1034
1035        /**
1036         * Store the buffered response at application level to use it at a later time.
1037         *
1038         * @param sessionId
1039         * @param url
1040         * @param response
1041         */
1042        public void storeBufferedResponse(String sessionId, Url url, BufferedWebResponse response)
1043        {
1044                if (Strings.isEmpty(sessionId))
1045                {
1046                        log.error("storeBufferedResponse needs a valid session id to store the response, but a null one was found. "
1047                                        + "Please report the problem to dev team and try to reproduce it in a quickstart project.");
1048                        return;
1049                }
1050
1051                String key = sessionId + url.toString();
1052                storedResponses.put(key, response);
1053        }
1054
1055        @Override
1056        public String getMimeType(String fileName)
1057        {
1058                String mimeType = getServletContext().getMimeType(fileName);
1059                return mimeType != null ? mimeType : super.getMimeType(fileName);
1060        }
1061
1062        /**
1063         * Returns the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1064         * 
1065         * @return the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1066         */
1067        public Function<Page, AjaxRequestTarget> getAjaxRequestTargetProvider()
1068        {
1069                return ajaxRequestTargetProvider;
1070        }
1071
1072        /**
1073         * Sets the provider for {@link org.apache.wicket.ajax.AjaxRequestTarget} objects.
1074         * 
1075         * @param ajaxRequestTargetProvider
1076         *            the new provider
1077         */
1078        public Application setAjaxRequestTargetProvider(
1079                Function<Page, AjaxRequestTarget> ajaxRequestTargetProvider)
1080        {
1081                this.ajaxRequestTargetProvider = ajaxRequestTargetProvider;
1082                return this;
1083        }
1084
1085        /**
1086         * Returns the registered {@link org.apache.wicket.ajax.AjaxRequestTarget.IListener} objects.
1087         * 
1088         * @return the registered {@link org.apache.wicket.ajax.AjaxRequestTarget.IListener} objects.
1089         */
1090        public AjaxRequestTargetListenerCollection getAjaxRequestTargetListeners()
1091        {
1092                return ajaxRequestTargetListeners;
1093        }
1094
1095        /**
1096         * @return True if at least one filter factory has been added.
1097         */
1098        public final boolean hasFilterFactoryManager()
1099        {
1100                return filterFactoryManager != null;
1101        }
1102
1103        /**
1104         * @return The filter factory manager
1105         */
1106        public final FilterFactoryManager getFilterFactoryManager()
1107        {
1108                if (filterFactoryManager == null)
1109                {
1110                        filterFactoryManager = new FilterFactoryManager();
1111                }
1112                return filterFactoryManager;
1113        }
1114        
1115        /**
1116         * TODO remove in Wicket 10
1117         * 
1118         * @deprecated use {@link #setCspSettings(ContentSecurityPolicySettings)} instead
1119         */
1120        protected ContentSecurityPolicySettings newCspSettings()
1121        {
1122                return new ContentSecurityPolicySettings(this);
1123        }
1124
1125        /**
1126         * Returns the {@link ContentSecurityPolicySettings} for this application. See
1127         * {@link ContentSecurityPolicySettings} and {@link CSPHeaderConfiguration} for instructions on
1128         * configuring the CSP for your specific needs.
1129         * 
1130         * @return The {@link ContentSecurityPolicySettings} for this application.
1131         * @see ContentSecurityPolicySettings
1132         * @see CSPHeaderConfiguration
1133         * 
1134         * TODO make final in Wicket 10
1135         */
1136        public ContentSecurityPolicySettings getCspSettings()
1137        {
1138                checkSettingsAvailable();
1139
1140                if (cspSettings == null)
1141                {
1142                        cspSettings = newCspSettings();
1143                }
1144                return cspSettings;
1145        }
1146
1147        /**
1148         * Set CSP settings.
1149         * 
1150         */
1151        public void setCspSettings(ContentSecurityPolicySettings cspSettings)
1152        {
1153                this.cspSettings = cspSettings;
1154        }
1155}