001    /**
002     * Copyright (c) 2000-2012 Liferay, Inc. All rights reserved.
003     *
004     * The contents of this file are subject to the terms of the Liferay Enterprise
005     * Subscription License ("License"). You may not use this file except in
006     * compliance with the License. You can obtain a copy of the License by
007     * contacting Liferay, Inc. See the License for the specific language governing
008     * permissions and limitations under the License, including but not limited to
009     * distribution rights of the Software.
010     *
011     *
012     *
013     */
014    
015    package com.liferay.portal.servlet.filters.strip;
016    
017    import com.liferay.portal.kernel.cache.key.CacheKeyGenerator;
018    import com.liferay.portal.kernel.cache.key.CacheKeyGeneratorUtil;
019    import com.liferay.portal.kernel.concurrent.ConcurrentLFUCache;
020    import com.liferay.portal.kernel.io.OutputStreamWriter;
021    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
022    import com.liferay.portal.kernel.log.Log;
023    import com.liferay.portal.kernel.log.LogFactoryUtil;
024    import com.liferay.portal.kernel.portlet.LiferayWindowState;
025    import com.liferay.portal.kernel.scripting.ScriptingException;
026    import com.liferay.portal.kernel.servlet.HttpHeaders;
027    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
028    import com.liferay.portal.kernel.servlet.StringServletResponse;
029    import com.liferay.portal.kernel.util.CharPool;
030    import com.liferay.portal.kernel.util.ContentTypes;
031    import com.liferay.portal.kernel.util.GetterUtil;
032    import com.liferay.portal.kernel.util.HttpUtil;
033    import com.liferay.portal.kernel.util.JavaConstants;
034    import com.liferay.portal.kernel.util.KMPSearch;
035    import com.liferay.portal.kernel.util.ParamUtil;
036    import com.liferay.portal.kernel.util.Validator;
037    import com.liferay.portal.servlet.filters.BasePortalFilter;
038    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
039    import com.liferay.portal.util.MinifierUtil;
040    import com.liferay.portal.util.PropsValues;
041    
042    import java.io.Writer;
043    
044    import java.nio.CharBuffer;
045    
046    import java.util.HashSet;
047    import java.util.Set;
048    import java.util.regex.Matcher;
049    import java.util.regex.Pattern;
050    
051    import javax.servlet.FilterChain;
052    import javax.servlet.FilterConfig;
053    import javax.servlet.http.HttpServletRequest;
054    import javax.servlet.http.HttpServletResponse;
055    
056    /**
057     * @author Brian Wing Shun Chan
058     * @author Raymond Augé
059     * @author Shuyang Zhou
060     */
061    public class StripFilter extends BasePortalFilter {
062    
063            public static final String SKIP_FILTER =
064                    StripFilter.class.getName() + "SKIP_FILTER";
065    
066            public StripFilter() {
067                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
068                            _minifierCache = new ConcurrentLFUCache<String, String>(
069                                    PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE);
070                    }
071            }
072    
073            @Override
074            public void init(FilterConfig filterConfig) {
075                    super.init(filterConfig);
076    
077                    for (String ignorePath : PropsValues.STRIP_IGNORE_PATHS) {
078                            _ignorePaths.add(ignorePath);
079                    }
080            }
081    
082            @Override
083            public boolean isFilterEnabled(
084                    HttpServletRequest request, HttpServletResponse response) {
085    
086                    if (isStrip(request) && !isInclude(request) &&
087                            !isAlreadyFiltered(request)) {
088    
089                            return true;
090                    }
091                    else {
092                            return false;
093                    }
094            }
095    
096            protected String extractContent(CharBuffer charBuffer, int length) {
097    
098                    // See LPS-10545
099    
100                    /*String content = charBuffer.subSequence(0, length).toString();
101    
102                    int position = charBuffer.position();
103    
104                    charBuffer.position(position + length);*/
105    
106                    CharBuffer duplicateCharBuffer = charBuffer.duplicate();
107    
108                    int position = duplicateCharBuffer.position() + length;
109    
110                    String content = duplicateCharBuffer.limit(position).toString();
111    
112                    charBuffer.position(position);
113    
114                    return content;
115            }
116    
117            protected boolean hasLanguageAttribute(
118                    CharBuffer charBuffer, int startPos, int length) {
119    
120                    if (!PropsValues.STRIP_JS_LANGUAGE_ATTRIBUTE_SUPPORT_ENABLED) {
121                            return false;
122                    }
123    
124                    if (KMPSearch.search(
125                                    charBuffer, startPos, length, _MARKER_LANGUAGE,
126                                    _MARKER_LANGUAGE_NEXTS) == -1) {
127    
128                            return false;
129                    }
130    
131                    Matcher matcher = _javaScriptPattern.matcher(charBuffer);
132    
133                    if (matcher.find()) {
134                            return true;
135                    }
136    
137                    return false;
138            }
139    
140            protected boolean hasMarker(CharBuffer charBuffer, char[] marker) {
141                    int position = charBuffer.position();
142    
143                    if ((position + marker.length) >= charBuffer.limit()) {
144                            return false;
145                    }
146    
147                    for (int i = 0; i < marker.length; i++) {
148                            char c = marker[i];
149    
150                            char oldC = charBuffer.charAt(i);
151    
152                            if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
153                                    return false;
154                            }
155                    }
156    
157                    return true;
158            }
159    
160            protected boolean isAlreadyFiltered(HttpServletRequest request) {
161                    if (request.getAttribute(SKIP_FILTER) != null) {
162                            return true;
163                    }
164                    else {
165                            return false;
166                    }
167            }
168    
169            protected boolean isInclude(HttpServletRequest request) {
170                    String uri = (String)request.getAttribute(
171                            JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
172    
173                    if (uri == null) {
174                            return false;
175                    }
176                    else {
177                            return true;
178                    }
179            }
180    
181            protected boolean isStrip(HttpServletRequest request) {
182                    if (!ParamUtil.getBoolean(request, _STRIP, true)) {
183                            return false;
184                    }
185    
186                    String path = request.getPathInfo();
187    
188                    if (_ignorePaths.contains(path)) {
189                            if (_log.isDebugEnabled()) {
190                                    _log.debug("Ignore path " + path);
191                            }
192    
193                            return false;
194                    }
195    
196                    // Modifying binary content through a servlet filter under certain
197                    // conditions is bad on performance the user will not start downloading
198                    // the content until the entire content is modified.
199    
200                    String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
201    
202                    if ((lifecycle.equals("1") &&
203                             LiferayWindowState.isExclusive(request)) ||
204                            lifecycle.equals("2")) {
205    
206                            return false;
207                    }
208                    else {
209                            return true;
210                    }
211            }
212    
213            protected void outputCloseTag(
214                            CharBuffer charBuffer, Writer writer, String closeTag)
215                    throws Exception {
216    
217                    writer.write(closeTag);
218    
219                    charBuffer.position(charBuffer.position() + closeTag.length());
220    
221                    skipWhiteSpace(charBuffer, writer, true);
222            }
223    
224            protected void outputOpenTag(
225                            CharBuffer charBuffer, Writer writer, char[] openTag)
226                    throws Exception {
227    
228                    writer.write(openTag);
229    
230                    charBuffer.position(charBuffer.position() + openTag.length);
231            }
232    
233            protected void processCSS(
234                            HttpServletRequest request, HttpServletResponse response,
235                            CharBuffer charBuffer, Writer writer)
236                    throws Exception {
237    
238                    outputOpenTag(charBuffer, writer, _MARKER_STYLE_OPEN);
239    
240                    int length = KMPSearch.search(
241                            charBuffer, _MARKER_STYLE_CLOSE, _MARKER_STYLE_CLOSE_NEXTS);
242    
243                    if (length == -1) {
244                            if (_log.isWarnEnabled()) {
245                                    _log.warn("Missing </style>");
246                            }
247    
248                            return;
249                    }
250    
251                    if (length == 0) {
252                            outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
253    
254                            return;
255                    }
256    
257                    String content = extractContent(charBuffer, length);
258    
259                    String minifiedContent = content;
260    
261                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
262                            CacheKeyGenerator cacheKeyGenerator =
263                                    CacheKeyGeneratorUtil.getCacheKeyGenerator(
264                                            StripFilter.class.getName());
265    
266                            String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
267    
268                            minifiedContent = _minifierCache.get(key);
269    
270                            if (minifiedContent == null) {
271                                    if (PropsValues.STRIP_CSS_SASS_ENABLED) {
272                                            try {
273                                                    content = DynamicCSSUtil.parseSass(
274                                                            request, key, content);
275                                            }
276                                            catch (ScriptingException se) {
277                                                    _log.error("Unable to parse SASS on CSS " + key, se);
278    
279                                                    if (_log.isDebugEnabled()) {
280                                                            _log.debug(content);
281                                                    }
282    
283                                                    if (response != null) {
284                                                            response.setHeader(
285                                                                    HttpHeaders.CACHE_CONTROL,
286                                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
287                                                    }
288                                            }
289                                    }
290    
291                                    minifiedContent = MinifierUtil.minifyCss(content);
292    
293                                    boolean skipCache = false;
294    
295                                    for (String skipCss :
296                                                    PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SKIP_CSS) {
297    
298                                            if (minifiedContent.contains(skipCss)) {
299                                                    skipCache = true;
300    
301                                                    break;
302                                            }
303                                    }
304    
305                                    if (!skipCache) {
306                                            _minifierCache.put(key, minifiedContent);
307                                    }
308                            }
309                    }
310    
311                    if (!Validator.isNull(minifiedContent)) {
312                            writer.write(minifiedContent);
313                    }
314    
315                    outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
316            }
317    
318            @Override
319            protected void processFilter(
320                            HttpServletRequest request, HttpServletResponse response,
321                            FilterChain filterChain)
322                    throws Exception {
323    
324                    if (_log.isDebugEnabled()) {
325                            String completeURL = HttpUtil.getCompleteURL(request);
326    
327                            _log.debug("Stripping " + completeURL);
328                    }
329    
330                    request.setAttribute(SKIP_FILTER, Boolean.TRUE);
331    
332                    StringServletResponse stringResponse = new StringServletResponse(
333                            response);
334    
335                    processFilter(StripFilter.class, request, stringResponse, filterChain);
336    
337                    String contentType = GetterUtil.getString(
338                            stringResponse.getContentType()).toLowerCase();
339    
340                    if (_log.isDebugEnabled()) {
341                            _log.debug("Stripping content of type " + contentType);
342                    }
343    
344                    response.setContentType(contentType);
345    
346                    if (contentType.startsWith(ContentTypes.TEXT_HTML) &&
347                            (stringResponse.getStatus() == HttpServletResponse.SC_OK)) {
348    
349                            CharBuffer oldCharBuffer = CharBuffer.wrap(
350                                    stringResponse.getString());
351    
352                            boolean ensureContentLength = ParamUtil.getBoolean(
353                                    request, _ENSURE_CONTENT_LENGTH);
354    
355                            if (ensureContentLength) {
356                                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
357                                            new UnsyncByteArrayOutputStream();
358    
359                                    strip(
360                                            request, response, oldCharBuffer,
361                                            new OutputStreamWriter(unsyncByteArrayOutputStream));
362    
363                                    response.setContentLength(unsyncByteArrayOutputStream.size());
364    
365                                    unsyncByteArrayOutputStream.writeTo(response.getOutputStream());
366                            }
367                            else {
368                                    strip(request, response, oldCharBuffer, response.getWriter());
369                            }
370                    }
371                    else {
372                            ServletResponseUtil.write(response, stringResponse);
373                    }
374            }
375    
376            protected void processInput(CharBuffer oldCharBuffer, Writer writer)
377                    throws Exception {
378    
379                    int length = KMPSearch.search(
380                            oldCharBuffer, _MARKER_INPUT_OPEN.length + 1, _MARKER_INPUT_CLOSE,
381                            _MARKER_INPUT_CLOSE_NEXTS);
382    
383                    if (length == -1) {
384                            if (_log.isWarnEnabled()) {
385                                    _log.warn("Missing />");
386                            }
387    
388                            outputOpenTag(oldCharBuffer, writer, _MARKER_INPUT_OPEN);
389    
390                            return;
391                    }
392    
393                    length += _MARKER_INPUT_CLOSE.length();
394    
395                    String content = extractContent(oldCharBuffer, length);
396    
397                    writer.write(content);
398    
399                    skipWhiteSpace(oldCharBuffer, writer, true);
400            }
401    
402            protected void processJavaScript(
403                            CharBuffer charBuffer, Writer writer, char[] openTag)
404                    throws Exception {
405    
406                    int endPos = openTag.length + 1;
407    
408                    char c = charBuffer.charAt(openTag.length);
409    
410                    if (c == CharPool.SPACE) {
411                            int startPos = openTag.length + 1;
412    
413                            for (int i = startPos; i < charBuffer.length(); i++) {
414                                    c = charBuffer.charAt(i);
415    
416                                    if (c == CharPool.GREATER_THAN) {
417    
418                                            // Open script tag complete
419    
420                                            endPos = i + 1;
421    
422                                            int length = i - startPos;
423    
424                                            if ((length < _MARKER_TYPE_JAVASCRIPT.length()) ||
425                                                    (KMPSearch.search(
426                                                            charBuffer, startPos, length,
427                                                            _MARKER_TYPE_JAVASCRIPT,
428                                                            _MARKER_TYPE_JAVASCRIPT_NEXTS) == -1)) {
429    
430                                                    // We have just determined that this is an open script
431                                                    // tag that does not have the attribute
432                                                    // type="text/javascript". Now check to see if it has
433                                                    // the attribute language="JavaScript". If it does not,
434                                                    // then we skip stripping.
435    
436                                                    if (!hasLanguageAttribute(
437                                                                    charBuffer, startPos, length)) {
438    
439                                                            return;
440                                                    }
441                                            }
442    
443                                            // Open script tag has no attribute or has attribute
444                                            // type="text/javascript". Start stripping.
445    
446                                            break;
447                                    }
448                                    else if (c == CharPool.LESS_THAN) {
449    
450                                            // Illegal open script tag. Found a '<' before seeing a '>'.
451    
452                                            return;
453                                    }
454                            }
455    
456                            if (endPos == charBuffer.length()) {
457    
458                                    // Illegal open script tag. Unable to find a '>'.
459    
460                                    return;
461                            }
462                    }
463                    else if (c != CharPool.GREATER_THAN) {
464    
465                            // Illegal open script tag. Not followed by a '>' or a ' '.
466    
467                            return;
468                    }
469    
470                    writer.append(charBuffer, 0, endPos);
471    
472                    charBuffer.position(charBuffer.position() + endPos);
473    
474                    int length = KMPSearch.search(
475                            charBuffer, _MARKER_SCRIPT_CLOSE, _MARKER_SCRIPT_CLOSE_NEXTS);
476    
477                    if (length == -1) {
478                            if (_log.isWarnEnabled()) {
479                                    _log.warn("Missing </script>");
480                            }
481    
482                            return;
483                    }
484    
485                    if (length == 0) {
486                            outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
487    
488                            return;
489                    }
490    
491                    String content = extractContent(charBuffer, length);
492    
493                    String minifiedContent = content;
494    
495                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
496                            CacheKeyGenerator cacheKeyGenerator =
497                                    CacheKeyGeneratorUtil.getCacheKeyGenerator(
498                                            StripFilter.class.getName());
499    
500                            String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
501    
502                            minifiedContent = _minifierCache.get(key);
503    
504                            if (minifiedContent == null) {
505                                    minifiedContent = MinifierUtil.minifyJavaScript(content);
506    
507                                    boolean skipCache = false;
508    
509                                    for (String skipJavaScript :
510                                                    PropsValues.
511                                                            MINIFIER_INLINE_CONTENT_CACHE_SKIP_JAVASCRIPT) {
512    
513                                            if (minifiedContent.contains(skipJavaScript)) {
514                                                    skipCache = true;
515    
516                                                    break;
517                                            }
518                                    }
519    
520                                    if (!skipCache) {
521                                            _minifierCache.put(key, minifiedContent);
522                                    }
523                            }
524                    }
525    
526                    if (!Validator.isNull(minifiedContent)) {
527                            writer.write(_CDATA_OPEN);
528                            writer.write(minifiedContent);
529                            writer.write(_CDATA_CLOSE);
530                    }
531    
532                    outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
533            }
534    
535            protected void processPre(CharBuffer oldCharBuffer, Writer writer)
536                    throws Exception {
537    
538                    int length = KMPSearch.search(
539                            oldCharBuffer, _MARKER_PRE_OPEN.length + 1, _MARKER_PRE_CLOSE,
540                            _MARKER_PRE_CLOSE_NEXTS);
541    
542                    if (length == -1) {
543                            if (_log.isWarnEnabled()) {
544                                    _log.warn("Missing </pre>");
545                            }
546    
547                            outputOpenTag(oldCharBuffer, writer, _MARKER_PRE_OPEN);
548    
549                            return;
550                    }
551    
552                    length += _MARKER_PRE_CLOSE.length();
553    
554                    String content = extractContent(oldCharBuffer, length);
555    
556                    writer.write(content);
557    
558                    skipWhiteSpace(oldCharBuffer, writer, true);
559            }
560    
561            protected void processTextArea(CharBuffer oldCharBuffer, Writer writer)
562                    throws Exception {
563    
564                    int length = KMPSearch.search(
565                            oldCharBuffer, _MARKER_TEXTAREA_OPEN.length + 1,
566                            _MARKER_TEXTAREA_CLOSE, _MARKER_TEXTAREA_CLOSE_NEXTS);
567    
568                    if (length == -1) {
569                            if (_log.isWarnEnabled()) {
570                                    _log.warn("Missing </textArea>");
571                            }
572    
573                            outputOpenTag(oldCharBuffer, writer, _MARKER_TEXTAREA_OPEN);
574                            return;
575                    }
576    
577                    length += _MARKER_TEXTAREA_CLOSE.length();
578    
579                    String content = extractContent(oldCharBuffer, length);
580    
581                    writer.write(content);
582    
583                    skipWhiteSpace(oldCharBuffer, writer, true);
584            }
585    
586            protected boolean skipWhiteSpace(
587                            CharBuffer charBuffer, Writer writer, boolean appendSeparator)
588                    throws Exception {
589    
590                    boolean skipped = false;
591    
592                    for (int i = charBuffer.position(); i < charBuffer.limit(); i++) {
593                            char c = charBuffer.get();
594    
595                            if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
596                                    (c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
597    
598                                    skipped = true;
599    
600                                    continue;
601                            }
602                            else {
603                                    charBuffer.position(i);
604    
605                                    break;
606                            }
607                    }
608    
609                    if (skipped && appendSeparator) {
610                            writer.write(CharPool.SPACE);
611                    }
612    
613                    return skipped;
614            }
615    
616            protected void strip(
617                            HttpServletRequest request, HttpServletResponse response,
618                            CharBuffer charBuffer, Writer writer)
619                    throws Exception {
620    
621                    skipWhiteSpace(charBuffer, writer, false);
622    
623                    while (charBuffer.hasRemaining()) {
624                            char c = charBuffer.get();
625    
626                            writer.write(c);
627    
628                            if (c == CharPool.LESS_THAN) {
629                                    if (hasMarker(charBuffer, _MARKER_INPUT_OPEN)) {
630                                            processInput(charBuffer, writer);
631    
632                                            continue;
633                                    }
634                                    else if (hasMarker(charBuffer, _MARKER_PRE_OPEN)) {
635                                            processPre(charBuffer, writer);
636    
637                                            continue;
638                                    }
639                                    else if (hasMarker(charBuffer, _MARKER_TEXTAREA_OPEN)) {
640                                            processTextArea(charBuffer, writer);
641    
642                                            continue;
643                                    }
644                                    else if (hasMarker(charBuffer, _MARKER_SCRIPT_OPEN)) {
645                                            processJavaScript(charBuffer, writer, _MARKER_SCRIPT_OPEN);
646    
647                                            continue;
648                                    }
649                                    else if (hasMarker(charBuffer, _MARKER_STYLE_OPEN)) {
650                                            processCSS(request, response, charBuffer, writer);
651    
652                                            continue;
653                                    }
654                            }
655                            else if (c == CharPool.GREATER_THAN) {
656                                    skipWhiteSpace(charBuffer, writer, true);
657                            }
658    
659                            skipWhiteSpace(charBuffer, writer, true);
660                    }
661    
662                    writer.flush();
663            }
664    
665            private static final String _CDATA_CLOSE = "/*]]>*/";
666    
667            private static final String _CDATA_OPEN = "/*<![CDATA[*/";
668    
669            private static final String _ENSURE_CONTENT_LENGTH = "ensureContentLength";
670    
671            private static final String _MARKER_INPUT_CLOSE = "/>";
672    
673            private static final int[] _MARKER_INPUT_CLOSE_NEXTS =
674                    KMPSearch.generateNexts(_MARKER_INPUT_CLOSE);
675    
676            private static final char[] _MARKER_INPUT_OPEN = "input".toCharArray();
677    
678            private static final String _MARKER_LANGUAGE = "language=\"";
679    
680            private static final int[] _MARKER_LANGUAGE_NEXTS = KMPSearch.generateNexts(
681                    _MARKER_LANGUAGE);
682    
683            private static final String _MARKER_PRE_CLOSE = "/pre>";
684    
685            private static final int[] _MARKER_PRE_CLOSE_NEXTS =
686                    KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
687    
688            private static final char[] _MARKER_PRE_OPEN = "pre".toCharArray();
689    
690            private static final String _MARKER_SCRIPT_CLOSE = "</script>";
691    
692            private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
693                    KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
694    
695            private static final char[] _MARKER_SCRIPT_OPEN = "script".toCharArray();
696    
697            private static final String _MARKER_STYLE_CLOSE = "</style>";
698    
699            private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
700                    KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
701    
702            private static final char[] _MARKER_STYLE_OPEN =
703                    "style type=\"text/css\">".toCharArray();
704    
705            private static final String _MARKER_TEXTAREA_CLOSE = "/textarea>";
706    
707            private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
708                    KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
709    
710            private static final char[] _MARKER_TEXTAREA_OPEN =
711                    "textarea ".toCharArray();
712    
713            private static final String _MARKER_TYPE_JAVASCRIPT =
714                    "type=\"text/javascript\"";
715    
716            private static final int[] _MARKER_TYPE_JAVASCRIPT_NEXTS =
717                    KMPSearch.generateNexts(_MARKER_TYPE_JAVASCRIPT);
718    
719            private static final String _STRIP = "strip";
720    
721            private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
722    
723            private static Pattern _javaScriptPattern = Pattern.compile(
724                    "[Jj][aA][vV][aA][sS][cC][rR][iI][pP][tT]");
725    
726            private Set<String> _ignorePaths = new HashSet<String>();
727            private ConcurrentLFUCache<String, String> _minifierCache;
728    
729    }