001/*
002 *  Copyright 2018 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.core.ui.resources.css;
017
018import java.net.URI;
019import java.net.URISyntaxException;
020import java.util.regex.Matcher;
021import java.util.regex.Pattern;
022
023import org.apache.commons.io.FilenameUtils;
024
025/**
026 * Helper for CSS Files
027 */
028public final class CSSFileHelper
029{
030    /** Regex that matches an @import statement in a css file, with optional media */
031    public static final Pattern IMPORT_PATTERN = Pattern.compile("(?<=^|\n|;)" // Matches the start of the line or end of previous statement
032                                                                + "[ \t]*@import[ \t]*" // Matches the '@import' mention
033                                                                + "(?:url)?" // Optional match with 'url'
034                                                                + "\\(?[ \t]*[\"']?" // Optional match with parenthesis, whitespace and/or quotes
035                                                                + "([^)\"'\n]*)" // Captures a string that does not contains a parenthesis or quote, the import url
036                                                                + "[\"']?\\)?[ \t]*" // Optional match with closing parenthesis, whitespace and/or quotes
037                                                                + "([^; \t\r\n]*)[ \t]*" // Captures an optional string, which is the "media" part of the @import
038                                                                + "(?:;|\n|$)?", // Matches optional end of statement or line 
039                                                                Pattern.CASE_INSENSITIVE);
040    
041    // This regex matches 'src=' declarations
042    private static final Pattern CSS_URL_PATTERN_SRC = Pattern.compile("src" // the string 'src' literally
043                                                                     + "\\s*=\\s*" // the symbol equal, with any whitespace before or after
044                                                                     + "['\"](.*?)['\"]", // capture a string in between quotes, singles or double
045                                                                     Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
046    
047    // This regex matches any 'url()' that is not preceded by an @import directive
048    private static final Pattern CSS_URL_PATTERN_NOTIMPORT_URL = Pattern.compile("(?:@import\\s+url)" // The pattern '@import url' that we want to ignore. When matching this, group(1) will be null
049                                                                               + "|" // The right side of this "or" is only evaluated when we don't match the previous pattern
050                                                                               
051                                                                               + "(?:"
052                                                                               + "\\burl" // The word 'url'
053                                                                               + "\\s*"
054                                                                               + "\\(" // The opening parenthesis before the url
055                                                                               + "\\s*['\"]?(.*?)['\"]?\\s*" // Capture a string that could be surrounded by whitespace and quotes
056                                                                               + "\\)" // The closing parenthesis after the url
057                                                                               + ")", 
058                                                                               Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
059    
060    // This regex matches any 'url()' that is preceded by an @import directive
061    private static final Pattern CSS_URL_PATTERN_IMPORT_URL = Pattern.compile("@import\\s+url\\s*" // The literal '@import url' with any number of whitespace
062                                                                            + "\\(" // The opening parenthesis of the 'url()'
063                                                                            + "\\s*['\"]?(.*?)['\"]?\\s*" // Capture a string that could be surrounded by whitespace and quotes
064                                                                            + "\\)", // The closing parenthesis of the 'url()'
065                                                                            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
066    
067    // This regex matches '@import' directives without 'url()'
068    private static final Pattern CSS_URL_PATTERN_IMPORT = Pattern.compile("@import\\s+" // The literal '@import' followed by whitespace
069                                                                        + "['\"](.*?)['\"]", // Capture a string that could be surrounded by quotes
070                                                                        Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
071    
072    private CSSFileHelper()
073    {
074    }
075    
076    /**
077     * Replace the relative URI inside a css file with the new context path
078     * @param content The file content
079     * @param fileUri The file Uri
080     * @param jsassResourceURIExtensionPoint The JSASS Resource URI extension point
081     * @param internalContextPath The internal context path of the application
082     * @param externalContextPath The external context path of the application
083     * @return The file content, with URIs replaced
084     * @throws URISyntaxException If an exception occurred
085     */
086    public static String replaceRelativeUri(String content, String fileUri, JSASSResourceURIExtensionPoint jsassResourceURIExtensionPoint, String internalContextPath, String externalContextPath) throws URISyntaxException
087    {
088        String c1 = _replaceRelativeUri(content, externalContextPath, fileUri, CSS_URL_PATTERN_SRC, jsassResourceURIExtensionPoint);
089        String c2 = _replaceRelativeUri(c1, externalContextPath, fileUri, CSS_URL_PATTERN_NOTIMPORT_URL, jsassResourceURIExtensionPoint);
090        String c3 = _replaceRelativeUri(c2, internalContextPath, fileUri, CSS_URL_PATTERN_IMPORT_URL, jsassResourceURIExtensionPoint);
091        String result = _replaceRelativeUri(c3, internalContextPath, fileUri, CSS_URL_PATTERN_IMPORT, jsassResourceURIExtensionPoint);
092
093        return result;
094    }
095    
096    /**
097     * Replace the relative URI inside a css file with the new context path
098     * @param content The file content
099     * @param fileUri The file Uri, without context path, for example "/plugins/pluginName/resources/style.css" or "/skins/skinName/resources/style.scss"
100     * @param jsassResourceURIExtensionPoint The JSASS Resource URI extension point
101     * @param externalContextPath The external context path of the application
102     * @return The file content, with URIs replaced
103     * @throws URISyntaxException If an exception occurred
104     */
105    public static String replaceRelativeResourcesUri(String content, String fileUri, JSASSResourceURIExtensionPoint jsassResourceURIExtensionPoint, String externalContextPath) throws URISyntaxException
106    {
107        String c1 = _replaceRelativeUri(content, externalContextPath, fileUri, CSS_URL_PATTERN_SRC, jsassResourceURIExtensionPoint);
108        String result = _replaceRelativeUri(c1, externalContextPath, fileUri, CSS_URL_PATTERN_NOTIMPORT_URL, jsassResourceURIExtensionPoint);
109
110        return result;
111    }
112    
113    private static String _replaceRelativeUri(String content, String contextPath, String fileUri, Pattern pattern, JSASSResourceURIExtensionPoint jsassResourceURIExtensionPoint)
114    {
115        Matcher urlMatcher = pattern.matcher(content);
116        StringBuffer sb = new StringBuffer();
117        
118        while (urlMatcher.find()) 
119        {
120            String fullMatch = urlMatcher.group();
121            String cssUrl = urlMatcher.group(1);
122            
123            if (cssUrl != null && !cssUrl.startsWith("data:"))
124            {
125                URI uri;
126                try
127                {
128                    uri = new URI(cssUrl.replaceAll("\\|", "%7C"));
129                    
130                    if (!uri.isAbsolute() && cssUrl.indexOf("/") != 0  && cssUrl.indexOf("#") != 0)
131                    {
132                        String fullFileUri = fileUri.indexOf('/') == 0 ? contextPath + fileUri : fileUri;
133                        String fullPathUrl = new URI(FilenameUtils.getFullPath(fullFileUri) + cssUrl).normalize().toString();
134                        urlMatcher.appendReplacement(sb, Matcher.quoteReplacement(fullMatch.replace(cssUrl, fullPathUrl)));
135                    }
136                    else
137                    {
138                        String relativeUri = jsassResourceURIExtensionPoint.resolve(cssUrl);
139                        if (relativeUri != null)
140                        {
141                            urlMatcher.appendReplacement(sb, Matcher.quoteReplacement(fullMatch.replace(cssUrl, relativeUri)));
142                        }
143                    }
144                }
145                catch (URISyntaxException e)
146                {
147                    urlMatcher.appendTail(sb);
148                    return sb.toString();
149                }
150            }
151        }
152        
153        urlMatcher.appendTail(sb);
154        
155        return sb.toString();
156    }
157
158}