View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.web.includes;
35  
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.net.URI;
39  import java.net.URISyntaxException;
40  import java.util.ArrayList;
41  import java.util.Collection;
42  import java.util.HashMap;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.stream.Collectors;
47  
48  import javax.servlet.ServletContext;
49  import javax.servlet.http.HttpServletRequest;
50  
51  import fr.paris.lutece.portal.business.style.Theme;
52  import fr.paris.lutece.portal.service.content.PageData;
53  import fr.paris.lutece.portal.service.includes.PageInclude;
54  import fr.paris.lutece.portal.service.plugin.Plugin;
55  import fr.paris.lutece.portal.service.plugin.PluginService;
56  import fr.paris.lutece.portal.service.portal.PortalService;
57  import fr.paris.lutece.portal.service.spring.SpringContextService;
58  import fr.paris.lutece.portal.service.template.AppTemplateService;
59  import fr.paris.lutece.portal.service.util.AppLogService;
60  import fr.paris.lutece.portal.service.util.CryptoService;
61  import fr.paris.lutece.portal.web.xpages.XPageApplicationEntry;
62  import fr.paris.lutece.util.html.HtmlTemplate;
63  
64  /**
65   * Page include that insert links into the head part of HTML pages
66   */
67  public class LinksInclude implements PageInclude
68  {
69      private static final String ALGORITHM = "SHA-256";
70  
71      // Parameters
72      private static final String PARAMETER_PAGE = "page";
73  
74      // Markers
75      private static final String MARK_FAVOURITE = "favourite";
76      private static final String MARK_PORTAL_NAME = "lutece_name";
77      private static final String MARK_PLUGIN_THEME_CSS = "plugin_theme";
78      private static final String MARK_PLUGINS_CSS_LINKS = "plugins_css_links";
79      private static final String MARK_PLUGINS_JAVASCRIPT_LINKS = "plugins_javascript_links";
80      private static final String MARK_PLUGIN_CSS_STYLESHEET = "plugin_css_stylesheet";
81      private static final String MARK_PLUGIN_JAVASCRIPT_FILE = "plugin_javascript_file";
82  
83      // Templates
84      private static final String TEMPLATE_PLUGIN_CSS_LINK = "skin/site/plugin_css_link.html";
85      private static final String TEMPLATE_PLUGIN_JAVASCRIPT_LINK = "skin/site/plugin_javascript_link.html";
86      private static final String PREFIX_PLUGINS_CSS = "css/plugins/";
87      private static final String PREFIX_PLUGINS_JAVASCRIPT = "js/plugins/";
88  
89      /**
90       * Substitue specific bookmarks in the page template.
91       * 
92       * @param rootModel
93       *            The data model
94       * @param data
95       *            A PageData object containing applications data
96       * @param nMode
97       *            The current mode
98       * @param request
99       *            The HTTP request
100      */
101     public void fillTemplate( Map<String, Object> rootModel, PageData data, int nMode, HttpServletRequest request )
102     {
103         if ( request == null )
104         {
105             return;
106         }
107         // Add links coming from the data object
108         String strFavourite = ( data.getFavourite( ) != null ) ? data.getFavourite( ) : PortalService.getSiteName( );
109         String strPortalName = PortalService.getSiteName( );
110         rootModel.put( MARK_FAVOURITE, strFavourite );
111         rootModel.put( MARK_PORTAL_NAME, strPortalName );
112 
113         // Add CSS links coming from plugins
114         Collection<Plugin> listPlugins = PluginService.getPluginList( );
115         listPlugins.add( PluginService.getCore( ) );
116 
117         String strPage = request.getParameter( PARAMETER_PAGE );
118 
119         List<Plugin> installedPlugins = listPlugins.stream( ).filter( Plugin::isInstalled ).collect( Collectors.toList( ) );
120 
121         for ( Plugin plugin : installedPlugins )
122         {
123             Theme xpageTheme = plugin.getXPageTheme( request );
124 
125             if ( ( strPage != null ) && ( xpageTheme != null ) )
126             {
127                 for ( XPageApplicationEntry entry : plugin.getApplications( ) )
128                 {
129                     if ( strPage.equals( entry.getId( ) ) )
130                     {
131                         rootModel.put( MARK_PLUGIN_THEME_CSS, xpageTheme );
132                     }
133                 }
134             }
135         }
136 
137         Map<String, Object> links = buildLinks( request, strPage, nMode, installedPlugins );
138         rootModel.putAll( links );
139     }
140 
141     private Map<String, Object> buildLinks( HttpServletRequest request, String strPage, int nMode, List<Plugin> installedPlugins )
142     {
143         Locale locale = request.getLocale( );
144         LinksIncludeCacheService cacheService = SpringContextService.getBean( LinksIncludeCacheService.SERVICE_NAME );
145         String strKey = cacheService.getCacheKey( nMode, strPage, locale );
146         @SuppressWarnings( "unchecked" )
147         Map<String, Object> links = (Map<String, Object>) cacheService.getFromCache( strKey );
148 
149         if ( links != null )
150         {
151             return links;
152         }
153         StringBuilder sbCssLinks = new StringBuilder( );
154         StringBuilder sbJsLinks = new StringBuilder( );
155 
156         for ( Plugin plugin : installedPlugins )
157         {
158             boolean bXPage = isPluginXPage( strPage, plugin );
159 
160             if ( plugin.isCssStylesheetsScopePortal( ) || ( bXPage && plugin.isCssStylesheetsScopeXPage( ) ) )
161             {
162                 List<String> cssFiles = new ArrayList<>( );
163                 cssFiles.addAll( plugin.getCssStyleSheets( ) );
164                 cssFiles.addAll( plugin.getCssStyleSheets( nMode ) );
165 
166                 cssFiles.stream( ).forEach( file -> appendStyleSheet( request.getServletContext( ), sbCssLinks, file, locale ) );
167             }
168 
169             if ( plugin.isJavascriptFilesScopePortal( ) || ( bXPage && plugin.isJavascriptFilesScopeXPage( ) ) )
170             {
171                 List<String> jsFiles = new ArrayList<>( );
172                 jsFiles.addAll( plugin.getJavascriptFiles( ) );
173                 jsFiles.addAll( plugin.getJavascriptFiles( nMode ) );
174 
175                 jsFiles.stream( ).forEach( file -> appendJavascriptFile( request.getServletContext( ), sbJsLinks, file, locale ) );
176             }
177         }
178 
179         links = new HashMap<>( 2 );
180         links.put( MARK_PLUGINS_CSS_LINKS, sbCssLinks.toString( ) );
181         links.put( MARK_PLUGINS_JAVASCRIPT_LINKS, sbJsLinks.toString( ) );
182         cacheService.putInCache( strKey, links );
183         return links;
184     }
185 
186     /**
187      * Append a script to the links
188      * 
189      * @param servletContext
190      *            servlet context
191      * @param sbJsLinks
192      *            links in construction
193      * @param strJavascriptFile
194      *            the script to append
195      * @param locale
196      *            the locale
197      */
198     private void appendJavascriptFile( ServletContext servletContext, StringBuilder sbJsLinks, String strJavascriptFile, Locale locale )
199     {
200         URI javascripFileURI = getURI( servletContext, strJavascriptFile, PREFIX_PLUGINS_JAVASCRIPT );
201 
202         if ( javascripFileURI == null )
203         {
204             return;
205         }
206 
207         Map<String, String> model = new HashMap<>( 1 );
208         model.put( MARK_PLUGIN_JAVASCRIPT_FILE, javascripFileURI.toString( ) );
209 
210         HtmlTemplate tJs = AppTemplateService.getTemplate( TEMPLATE_PLUGIN_JAVASCRIPT_LINK, locale, model );
211         sbJsLinks.append( tJs.getHtml( ) );
212     }
213 
214     /**
215      * Append a css to the stylesheets
216      * 
217      * @param servletContext
218      *            servlet context
219      * @param sbCssLinks
220      *            stylesheets in construction
221      * @param strCssStyleSheet
222      *            the stylesheet to append
223      * @param locale
224      *            the locale
225      */
226     private void appendStyleSheet( ServletContext servletContext, StringBuilder sbCssLinks, String strCssStyleSheet, Locale locale )
227     {
228         URI styleSheetURI = getURI( servletContext, strCssStyleSheet, PREFIX_PLUGINS_CSS );
229 
230         if ( styleSheetURI == null )
231         {
232             return;
233         }
234 
235         Map<String, String> model = new HashMap<>( 2 );
236         model.put( MARK_PLUGIN_CSS_STYLESHEET, styleSheetURI.toString( ) );
237 
238         HtmlTemplate tCss = AppTemplateService.getTemplate( TEMPLATE_PLUGIN_CSS_LINK, locale, model );
239         sbCssLinks.append( tCss.getHtml( ) );
240     }
241 
242     /**
243      * Get a URI for a resource. If the resource is provided by this site, a hash of its content is added as query parameter so that changes to the content are
244      * picked up by browsers.
245      * 
246      * @param servletContext
247      *            the servlet context
248      * @param strResourceURI
249      *            the resource URI as string
250      * @param strURIPrefix
251      *            a prefix to add to the URI if it is not absolute
252      * @return the URI or <code>null</code> if it cannot be parsed
253      */
254     private URI getURI( ServletContext servletContext, String strResourceURI, String strURIPrefix )
255     {
256         try
257         {
258             URI resourceURI = new URI( strResourceURI );
259             if ( !resourceURI.isAbsolute( ) && resourceURI.getHost( ) == null )
260             {
261                 if ( strURIPrefix != null )
262                 {
263                     resourceURI = new URI( strURIPrefix + strResourceURI );
264                 }
265                 resourceURI = addHashToUri( servletContext, resourceURI, strResourceURI );
266             }
267             return resourceURI;
268         }
269         catch( URISyntaxException e )
270         {
271             AppLogService.error( "Invalid cssStyleSheetURI : {}", strResourceURI, e );
272             return null;
273         }
274     }
275 
276     private URI addHashToUri( ServletContext servletContext, URI resourceURI, String strResourceURI ) throws URISyntaxException
277     {
278         try ( InputStream inputStream = servletContext.getResourceAsStream( resourceURI.getPath( ) ) )
279         {
280             if ( inputStream != null )
281             {
282                 String hash = CryptoService.digest( inputStream, ALGORITHM );
283                 if ( hash != null )
284                 {
285                     char separator = '?';
286                     if ( resourceURI.getQuery( ) != null )
287                     {
288                         separator = '&';
289                     }
290                     resourceURI = new URI( resourceURI.toString( ) + separator + "lutece_h=" + hash );
291                 }
292             }
293         }
294         catch( IOException e )
295         {
296             AppLogService.error( "Error while closing stream for {}", strResourceURI, e );
297         }
298         return resourceURI;
299     }
300 
301     /**
302      * Check if the page is a valid plugin's page
303      * 
304      * @param strPage
305      *            The page
306      * @param plugin
307      *            The plugin
308      * @return true if valid otherwise false
309      */
310     private boolean isPluginXPage( String strPage, Plugin plugin )
311     {
312         if ( ( strPage != null ) )
313         {
314             for ( XPageApplicationEntry app : plugin.getApplications( ) )
315             {
316                 if ( strPage.equals( app.getId( ) ) )
317                 {
318                     return true;
319                 }
320             }
321         }
322 
323         return false;
324     }
325 }