| /* |
| * Copyright 2010 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Author: mdsteele@google.com (Matthew D. Steele) |
| |
| #include "net/instaweb/rewriter/public/js_inline_filter.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/rewrite_test_base.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "pagespeed/kernel/base/basictypes.h" |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/html/html_parse_test_base.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| #include "pagespeed/kernel/http/semantic_type.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| class JsInlineFilterTest : public RewriteTestBase { |
| public: |
| JsInlineFilterTest() : filters_added_(false) {} |
| |
| protected: |
| // TODO(matterbury): Delete this method as it should be redundant. |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| } |
| |
| void TestInlineJavascript(const GoogleString& html_url, |
| const GoogleString& js_url, |
| const GoogleString& js_original_inline_body, |
| const GoogleString& js_outline_body, |
| bool expect_inline) { |
| TestInlineJavascriptGeneral( |
| html_url, |
| "", // don't use a doctype for these tests |
| js_url, |
| js_url, |
| js_original_inline_body, |
| js_outline_body, |
| js_outline_body, // expect ouline body to be inlined verbatim |
| expect_inline, |
| ""); |
| } |
| |
| void TestNoInlineJavascript(const GoogleString& html_url, |
| const GoogleString& js_url, |
| const GoogleString& js_original_inline_body, |
| const GoogleString& js_outline_body, |
| const GoogleString& debug_message) { |
| TestInlineJavascriptGeneral( |
| html_url, |
| "", // don't use a doctype for these tests |
| js_url, |
| js_url, |
| js_original_inline_body, |
| js_outline_body, |
| js_outline_body, // expect ouline body to be inlined verbatim |
| false, // Not inlined |
| debug_message); |
| } |
| |
| void TestInlineJavascriptXhtml(const GoogleString& html_url, |
| const GoogleString& js_url, |
| const GoogleString& js_outline_body, |
| bool expect_inline) { |
| TestInlineJavascriptGeneral( |
| html_url, |
| kXhtmlDtd, |
| js_url, |
| js_url, |
| "", // use an empty original inline body for these tests |
| js_outline_body, |
| // Expect outline body to get surrounded by a CDATA block: |
| "//<![CDATA[\n" + js_outline_body + "\n//]]>", |
| expect_inline, |
| ""); |
| } |
| |
| void TestInlineJavascriptGeneral(const GoogleString& html_url, |
| const GoogleString& doctype, |
| const GoogleString& js_url, |
| const GoogleString& js_out_url, |
| const GoogleString& js_original_inline_body, |
| const GoogleString& js_outline_body, |
| const GoogleString& js_expected_inline_body, |
| bool expect_inline, |
| const GoogleString& debug_string) { |
| if (!filters_added_) { |
| options()->SoftEnableFilterForTesting(RewriteOptions::kInlineJavascript); |
| rewrite_driver()->AddFilters(); |
| filters_added_ = true; |
| } |
| |
| // Specify the input and expected output. |
| if (!doctype.empty()) { |
| SetDoctype(doctype); |
| } |
| |
| const char kHtmlTemplate[] = |
| "<head>\n" |
| " <script src=\"%s\">%s</script>%s\n" |
| "</head>\n" |
| "<body>Hello, world!</body>\n"; |
| |
| const GoogleString html_input = |
| StringPrintf(kHtmlTemplate, js_url.c_str(), |
| js_original_inline_body.c_str(), ""); |
| |
| const GoogleString outline_html_output = |
| StringPrintf(kHtmlTemplate, js_out_url.c_str(), |
| js_original_inline_body.c_str(), ""); |
| |
| const GoogleString expected_output = |
| (!expect_inline ? outline_html_output : |
| "<head>\n" |
| " <script>" + js_expected_inline_body + "</script>\n" |
| "</head>\n" |
| "<body>Hello, world!</body>\n"); |
| |
| const GoogleString outline_debug_html_output = debug_string.empty() |
| ? outline_html_output |
| : StringPrintf(kHtmlTemplate, js_out_url.c_str(), |
| js_original_inline_body.c_str(), |
| StrCat("<!--", debug_string, "-->").c_str()); |
| |
| // Put original Javascript file into our fetcher. |
| ResponseHeaders default_js_header; |
| SetDefaultLongCacheHeaders(&kContentTypeJavascript, &default_js_header); |
| SetFetchResponse(js_url, default_js_header, js_outline_body); |
| |
| // Rewrite the HTML page. |
| ValidateExpectedUrl(html_url, html_input, expected_output); |
| |
| if (!expect_inline) { |
| TurnOnDebug(); |
| ValidateExpectedUrl(html_url, html_input, |
| outline_debug_html_output); |
| } |
| } |
| |
| void TurnOnDebug() { |
| options()->ClearSignatureForTesting(); |
| options()->ForceEnableFilter(RewriteOptions::kDebug); |
| server_context()->ComputeSignature(options()); |
| } |
| |
| private: |
| bool filters_added_; |
| }; |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptNoMimetype) { |
| // Simple case: |
| TestInlineJavascriptXhtml("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "function id(x) { return x; }\n", |
| true); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptSimpleHtml) { |
| SetHtmlMimetype(); |
| |
| // Simple case: |
| TestInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| "function id(x) { return x; }\n", |
| true); |
| } |
| |
| class JsInlineFilterTestCustomOptions : public JsInlineFilterTest { |
| protected: |
| virtual void SetUp() {} |
| }; |
| |
| TEST_F(JsInlineFilterTestCustomOptions, InlineJsPreserveURLSOn) { |
| // Make sure that we don't inline when preserve urls is on. |
| options()->set_js_preserve_urls(true); |
| JsInlineFilterTest::SetUp(); |
| SetHtmlMimetype(); |
| |
| // Simple case: |
| TestInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| "function id(x) { return x; }\n", |
| false); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptSimpleXhtml) { |
| SetXhtmlMimetype(); |
| |
| // Simple case: |
| TestInlineJavascriptXhtml("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "function id(x) { return x; }\n", |
| true); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptWhitespace) { |
| SetHtmlMimetype(); |
| |
| // Whitespace between <script> and </script>: |
| TestInlineJavascript("http://www.example.com/index2.html", |
| "http://www.example.com/script2.js", |
| "\n \n ", |
| "function id(x) { return x; }\n", |
| true); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptDifferentDomain) { |
| options()->AddInlineUnauthorizedResourceType(semantic_type::kScript); |
| SetHtmlMimetype(); |
| TestInlineJavascript("http://www.example.net/index.html", |
| "http://scripts.example.org/script2.js", |
| "", |
| "function id(x) { return x; }\n", |
| true); |
| EXPECT_EQ(1, statistics()->GetVariable(JsInlineFilter::kNumJsInlined)->Get()); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoNotInlineJavascriptDifferentDomain) { |
| // Different domains: |
| GoogleUrl gurl("http://scripts.example.org/script.js"); |
| TestNoInlineJavascript("http://www.example.net/index.html", |
| gurl.Spec().as_string(), |
| "", |
| "function id(x) { return x; }\n", |
| RewriteDriver::GenerateUnauthorizedDomainDebugComment( |
| gurl)); |
| EXPECT_EQ(0, statistics()->GetVariable(JsInlineFilter::kNumJsInlined)->Get()); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoNotInlineJavascriptInlineContents) { |
| // Inline contents: |
| TestInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "{\"json\": true}", |
| "function id(x) { return x; }\n", |
| false); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoNotInlineJavascriptTooBig) { |
| // Javascript too long: |
| const int64 length = 2 * RewriteOptions::kDefaultJsInlineMaxBytes; |
| TestNoInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| ("function longstr() { return '" + |
| GoogleString(length, 'z') + "'; }\n"), |
| "JS not inlined since it's bigger than 2048 bytes"); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoNotInlineIntrospectiveJavascriptByDefault) { |
| // If it's unsafe to rename, because it contains fragile introspection like |
| // $("script"), we have to leave it at the original url and not inline it. |
| // Dependent on a config option that's on by default. |
| TestNoInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| "function close() { return $('script'); }\n", |
| "JS not inlined since it may be looking for " |
| "its source"); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineIntrospectiveJavascript) { |
| options()->set_avoid_renaming_introspective_javascript(false); |
| SetHtmlMimetype(); |
| |
| // The same situation as DoNotInlineIntrospectiveJavascript, but in the |
| // default configuration we want to be sure we're still inlining. |
| TestInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| "function close() { return $('script'); }\n", |
| true); // expect inlining |
| } |
| |
| TEST_F(JsInlineFilterTest, DontInlineDisallowed) { |
| SetHtmlMimetype(); |
| |
| options()->Disallow("*script.js*"); |
| |
| // The script is disallowed; can't be inlined. |
| GoogleUrl gurl("http://www.example.com/script.js"); |
| TestNoInlineJavascript("http://www.example.com/index.html", |
| gurl.Spec().as_string(), |
| "", |
| "function close() { return 'inline!'; }\n", |
| RewriteDriver::GenerateUnauthorizedDomainDebugComment( |
| gurl)); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineDisallowedIfAllowedWhenInlining) { |
| SetHtmlMimetype(); |
| options()->AllowOnlyWhenInlining("*script.js*"); |
| |
| // The script is allowed when inlining. |
| TestInlineJavascript("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "", |
| "function close() { return 'inline!'; }\n", |
| true); // expect inlining |
| } |
| |
| TEST_F(JsInlineFilterTest, DoInlineJavascriptXhtml) { |
| // Simple case: |
| TestInlineJavascriptXhtml("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "function id(x) { return x; }\n", |
| true); |
| } |
| |
| TEST_F(JsInlineFilterTest, DoNotInlineJavascriptXhtmlWithCdataEnd) { |
| // External script contains "]]>": |
| TestInlineJavascriptXhtml("http://www.example.com/index.html", |
| "http://www.example.com/script.js", |
| "function end(x) { return ']]>'; }\n", |
| false); |
| } |
| |
| TEST_F(JsInlineFilterTest, CachedRewrite) { |
| // Make sure we work fine when result is cached. |
| const char kPageUrl[] = "http://www.example.com/index.html"; |
| const char kJsUrl[] = "http://www.example.com/script.js"; |
| const char kJs[] = "function id(x) { return x; }\n"; |
| const char kNothingInsideScript[] = ""; |
| SetHtmlMimetype(); |
| TestInlineJavascript(kPageUrl, kJsUrl, kNothingInsideScript, kJs, true); |
| TestInlineJavascript(kPageUrl, kJsUrl, kNothingInsideScript, kJs, true); |
| } |
| |
| TEST_F(JsInlineFilterTest, CachedWithSuccesors) { |
| SetHtmlMimetype(); |
| |
| // Regression test: in async case, at one point we had a problem with |
| // slot rendering of a following cache extender trying to manipulate |
| // the source attribute which the inliner deleted while using |
| // cached filter results. |
| SetHtmlMimetype(); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kInlineJavascript); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kExtendCacheScripts); |
| rewrite_driver()->AddFilters(); |
| |
| const char kJsUrl[] = "script.js"; |
| const char kJs[] = "function id(x) { return x; }\n"; |
| |
| SetResponseWithDefaultHeaders(kJsUrl, kContentTypeJavascript, kJs, 3000); |
| |
| GoogleString html_input = StrCat("<script src=\"", kJsUrl, "\"></script>"); |
| GoogleString html_output= StrCat("<script>", kJs, "</script>"); |
| |
| ValidateExpected("inline_with_succ", html_input, html_output); |
| ValidateExpected("inline_with_succ", html_input, html_output); |
| } |
| |
| TEST_F(JsInlineFilterTest, CachedWithPredecessors) { |
| // Regression test for crash: trying to inline after combining would crash. |
| // (Current state is not to inline after combining due to the |
| // <script> element with src= being new). |
| SetHtmlMimetype(); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kInlineJavascript); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kCombineJavascript); |
| rewrite_driver()->AddFilters(); |
| |
| const char kJsUrl[] = "script.js"; |
| const char kJs[] = "function id(x) { return x; }\n"; |
| |
| SetResponseWithDefaultHeaders(kJsUrl, kContentTypeJavascript, kJs, 3000); |
| |
| GoogleString html_input = StrCat("<script src=\"", kJsUrl, "\"></script>", |
| "<script src=\"", kJsUrl, "\"></script>"); |
| |
| Parse("inline_with_pred", html_input); |
| Parse("inline_with_pred", html_input); |
| } |
| |
| TEST_F(JsInlineFilterTest, InlineJs404) { |
| // Test to make sure that a missing input is handled well. |
| SetHtmlMimetype(); |
| SetFetchResponse404("404.js"); |
| AddFilter(RewriteOptions::kInlineJavascript); |
| ValidateNoChanges("404", "<script src='404.js'></script>"); |
| |
| // Second time, to make sure caching doesn't break it. |
| ValidateNoChanges("404", "<script src='404.js'></script>"); |
| } |
| |
| TEST_F(JsInlineFilterTest, InlineMinimizeInteraction) { |
| // There was a bug in async mode where we would accidentally prevent |
| // minification results from rendering when inlining was not to be done. |
| SetHtmlMimetype(); |
| options()->SoftEnableFilterForTesting( |
| RewriteOptions::kRewriteJavascriptExternal); |
| options()->SoftEnableFilterForTesting( |
| RewriteOptions::kRewriteJavascriptInline); |
| options()->set_js_inline_max_bytes(4); |
| |
| TestInlineJavascriptGeneral( |
| StrCat(kTestDomain, "minimize_but_not_inline.html"), |
| "", // No doctype |
| StrCat(kTestDomain, "a.js"), |
| // Note: Original URL was absolute, so rewritten one is as well. |
| Encode(kTestDomain, "jm", "0", "a.js", "js"), |
| "", // No inline body in, |
| "var answer = 42; // const is non-standard", // out-of-line body |
| "", // No inline body out, |
| false, // Not inlining |
| "JS not inlined since it's bigger than 4 bytes"); |
| } |
| |
| TEST_F(JsInlineFilterTest, ScriptWithScriptTags) { |
| SetHtmlMimetype(); |
| AddFilter(RewriteOptions::kInlineJavascript); |
| |
| ResponseHeaders default_js_header; |
| SetDefaultLongCacheHeaders(&kContentTypeJavascript, &default_js_header); |
| GoogleString js_url = StrCat(kTestDomain, "a.js"); |
| SetFetchResponse(js_url, |
| default_js_header, |
| "alert('<script></script>');" |
| "alert('<sCrIpT></ScRiPt>');" |
| "alert('</SCRIPT foo>');" |
| "alert('<Script</sCRIPT');" |
| "alert('</scr>');"); |
| |
| // a.js now contains a script that needs escaping to inline. |
| |
| ValidateExpectedUrl( |
| StrCat(kTestDomain, "inline_with_close_script.html"), |
| |
| // Input, with js referenced externally. |
| StringPrintf( |
| "<head>\n" |
| " <script src='%s'></script>\n" |
| "</head>\n" |
| "<body>Hello, world!</body>\n", |
| js_url.c_str()), |
| |
| // Expected output, with js inlined and escaped. |
| "<head>\n" |
| " <script>alert('<\\u0073cript></\\u0073cript>');" |
| "alert('<\\u0073CrIpT></\\u0053cRiPt>');" |
| "alert('</\\u0053CRIPT foo>');" |
| "alert('<\\u0053cript</\\u0073CRIPT');" |
| "alert('</scr>');</script>\n" |
| "</head>\n" |
| "<body>Hello, world!</body>\n"); |
| } |
| |
| TEST_F(JsInlineFilterTest, FlushSplittingScriptTag) { |
| SetHtmlMimetype(); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kInlineJavascript); |
| rewrite_driver()->AddFilters(); |
| SetupWriter(); |
| |
| const char kJsUrl[] = "http://www.example.com/script.js"; |
| const char kJs[] = "function id(x) { return x; }\n"; |
| SetResponseWithDefaultHeaders(kJsUrl, kContentTypeJavascript, kJs, 3000); |
| |
| html_parse()->StartParse("http://www.example.com"); |
| html_parse()->ParseText("<div><script src=\"script.js\"> "); |
| html_parse()->Flush(); |
| html_parse()->ParseText("</script> </div>"); |
| html_parse()->FinishParse(); |
| EXPECT_STREQ("<div><script>function id(x) { return x; }\n</script> </div>", |
| output_buffer_); |
| } |
| |
| TEST_F(JsInlineFilterTest, NoFlushSplittingScriptTag) { |
| SetHtmlMimetype(); |
| options()->SoftEnableFilterForTesting(RewriteOptions::kInlineJavascript); |
| rewrite_driver()->AddFilters(); |
| SetupWriter(); |
| |
| const char kJsUrl[] = "http://www.example.com/script.js"; |
| const char kJs[] = "function id(x) { return x; }\n"; |
| SetResponseWithDefaultHeaders(kJsUrl, kContentTypeJavascript, kJs, 3000); |
| |
| html_parse()->StartParse("http://www.example.com"); |
| html_parse()->ParseText("<div><script src=\"script.js\"> "); |
| html_parse()->ParseText(" </script> </div>"); |
| html_parse()->FinishParse(); |
| EXPECT_STREQ("<div><script>function id(x) { return x; }\n</script> </div>", |
| output_buffer_); |
| } |
| |
| } // namespace |
| |
| } // namespace net_instaweb |