blob: 078c376eb764c83111a255b0c1e3f6e45e74d0bc [file] [log] [blame]
/*
* Copyright 2013 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: jud@google.com (Jud Porter)
#include "net/instaweb/rewriter/public/critical_images_beacon_filter.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/public/global_constants.h"
#include "net/instaweb/rewriter/public/beacon_critical_images_finder.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/lazyload_images_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 "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/escaping.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_hash.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/user_agent_matcher_test_base.h"
#include "pagespeed/opt/logging/enums.pb.h"
using testing::HasSubstr;
using testing::Not;
namespace {
const char kChefGifFile[] = "IronChef2.gif";
// Set image dimensions such that image will be inlined.
const char kChefGifDims[] = "width=48 height=64";
const char kRequestUrl[] = "http://www.example.com";
} // namespace
namespace net_instaweb {
class CriticalImagesBeaconFilterTest : public RewriteTestBase {
protected:
CriticalImagesBeaconFilterTest() {}
virtual void SetUp() {
options()->set_beacon_url("http://example.com/beacon");
CriticalImagesBeaconFilter::InitStats(statistics());
// Enable a filter that uses critical images, which in turn will enable
// beacon insertion.
factory()->set_use_beacon_results_in_filters(true);
options()->EnableFilter(RewriteOptions::kDelayImages);
RewriteTestBase::SetUp();
https_mode_ = false;
// Setup the property cache. The DetermineEnable logic for the
// CriticalImagesBeaconFinder will only inject the beacon if the property
// cache is enabled, since beaconed results are intended to be stored in the
// pcache.
PropertyCache* pcache = page_property_cache();
server_context_->set_enable_property_cache(true);
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(pcache, RewriteDriver::kBeaconCohort);
const PropertyCache::Cohort* dom_cohort =
SetupCohort(pcache, RewriteDriver::kDomCohort);
server_context()->set_beacon_cohort(beacon_cohort);
server_context()->set_dom_cohort(dom_cohort);
server_context()->set_critical_images_finder(
new BeaconCriticalImagesFinder(
beacon_cohort, factory()->nonce_generator(), statistics()));
GoogleUrl base(GetTestUrl());
image_gurl_.Reset(base, kChefGifFile);
ResetDriver();
}
void ResetDriver() {
rewrite_driver()->Clear();
SetCurrentUserAgent(UserAgentMatcherTestBase::kChrome18UserAgent);
rewrite_driver()->set_request_context(
RequestContext::NewTestRequestContext(factory()->thread_system()));
MockPropertyPage* page = NewMockPage(kRequestUrl);
rewrite_driver()->set_property_page(page);
PropertyCache* pcache = server_context_->page_property_cache();
pcache->Read(page);
}
void WriteToPropertyCache() {
rewrite_driver()->property_page()->WriteCohort(
server_context()->beacon_cohort());
}
void PrepareInjection() {
rewrite_driver()->AddFilters();
AddFileToMockFetcher(image_gurl_.Spec(), kChefGifFile,
kContentTypeJpeg, 100);
}
void AddImageTags(GoogleString* html) {
// Add the relative image URL.
StrAppend(html, "<img src=\"", kChefGifFile, "\" ", kChefGifDims, ">");
// Add the absolute image URL.
StrAppend(html, "<img src=\"", image_gurl_.Spec(), "\" ",
kChefGifDims, ">");
}
void RunInjection() {
PrepareInjection();
SetupAndProcessUrl();
}
void SetupAndProcessUrl() {
GoogleString html = "<head></head><body>";
AddImageTags(&html);
StrAppend(&html, "</body>");
ParseUrl(GetTestUrl(), html);
}
void RunInjectionNoBody() {
// As above, but we omit <head> and (more relevant) <body> tags. We should
// still inject the script at the end of the document. The filter used to
// get this wrong.
PrepareInjection();
GoogleString html;
AddImageTags(&html);
ParseUrl(GetTestUrl(), html);
}
void VerifyInjection(int expected_beacon_count) {
EXPECT_EQ(expected_beacon_count, statistics()->GetVariable(
CriticalImagesBeaconFilter::kCriticalImagesBeaconAddedCount)->Get());
EXPECT_THAT(output_buffer_, HasSubstr(CreateInitString()));
}
void VerifyNoInjection(int expected_beacon_count) {
EXPECT_EQ(expected_beacon_count, statistics()->GetVariable(
CriticalImagesBeaconFilter::kCriticalImagesBeaconAddedCount)->Get());
EXPECT_THAT(output_buffer_, Not(HasSubstr("pagespeed.CriticalImages.Run")));
}
void VerifyWithNoImageRewrite() {
const GoogleString hash_str = ImageUrlHash(kChefGifFile);
EXPECT_THAT(output_buffer_,
HasSubstr(StrCat("data-pagespeed-url-hash=\"", hash_str)));
}
void AssumeHttps() {
https_mode_ = true;
}
GoogleString GetTestUrl() {
return StrCat((https_mode_ ? "https://example.com/" : kTestDomain),
"index.html?a&b");
}
GoogleString ImageUrlHash(StringPiece url) {
// Absolutify the URL before hashing.
unsigned int hash_val = HashString<CasePreserve, unsigned int>(
image_gurl_.spec_c_str(), strlen(image_gurl_.spec_c_str()));
return UintToString(hash_val);
}
GoogleString CreateInitString() {
GoogleString url;
EscapeToJsStringLiteral(rewrite_driver()->google_url().Spec(), false, &url);
StringPiece beacon_url = https_mode_ ? options()->beacon_url().https :
options()->beacon_url().http;
GoogleString options_signature_hash =
rewrite_driver()->server_context()->hasher()->Hash(
rewrite_driver()->options()->signature());
bool lazyload_will_run_beacon =
rewrite_driver()->options()->Enabled(RewriteOptions::kLazyloadImages) &&
LazyloadImagesFilter::ShouldApply(rewrite_driver()) ==
RewriterHtmlApplication::ACTIVE;
GoogleString str = "pagespeed.CriticalImages.Run(";
StrAppend(&str, "'", beacon_url, "',");
StrAppend(&str, "'", url, "',");
StrAppend(&str, "'", options_signature_hash, "',");
StrAppend(&str, BoolToString(!lazyload_will_run_beacon), ",");
StrAppend(&str, BoolToString(rewrite_driver()->options()->Enabled(
RewriteOptions::kResizeToRenderedImageDimensions)),
",");
StrAppend(&str, "'", ExpectedNonce(), "');");
return str;
}
bool https_mode_;
GoogleUrl image_gurl_;
};
TEST_F(CriticalImagesBeaconFilterTest, ScriptInjection) {
RunInjection();
VerifyInjection(1);
// Verify that image onload criticality check has been added.
int img_begin = output_buffer_.find("IronChef2");
EXPECT_TRUE(img_begin != GoogleString::npos);
int img_end = output_buffer_.substr(img_begin).find(">");
EXPECT_TRUE(img_end != GoogleString::npos);
EXPECT_TRUE(output_buffer_.substr(img_begin, img_end).find(
"onload=\"pagespeed.CriticalImages."
"checkImageForCriticality(this);\"") !=
GoogleString::npos);
VerifyWithNoImageRewrite();
}
TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionNoBody) {
RunInjectionNoBody();
VerifyInjection(1);
VerifyWithNoImageRewrite();
}
TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionWithHttps) {
AssumeHttps();
RunInjection();
VerifyInjection(1);
VerifyWithNoImageRewrite();
}
TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionWithImageInlining) {
// Verify that the URL hash is applied to the absolute image URL, and not to
// the rewritten URL. In this case, make sure that an image inlined to a data
// URI has the correct hash. We need to add the image hash to the critical
// image set to make sure that the image is inlined.
GoogleString hash_str = ImageUrlHash(kChefGifFile);
StringSet* crit_img_set = server_context()->critical_images_finder()->
mutable_html_critical_images(rewrite_driver());
crit_img_set->insert(hash_str);
options()->set_image_inline_max_bytes(10000);
options()->EnableFilter(RewriteOptions::kResizeImages);
options()->EnableFilter(RewriteOptions::kResizeToRenderedImageDimensions);
options()->EnableFilter(RewriteOptions::kInlineImages);
options()->EnableFilter(RewriteOptions::kInsertImageDimensions);
options()->EnableFilter(RewriteOptions::kConvertGifToPng);
options()->DisableFilter(RewriteOptions::kDelayImages);
RunInjection();
VerifyInjection(1);
EXPECT_TRUE(output_buffer_.find("data:") != GoogleString::npos);
EXPECT_TRUE(output_buffer_.find(hash_str) != GoogleString::npos);
EXPECT_EQ(-1, logging_info()->num_html_critical_images());
EXPECT_EQ(-1, logging_info()->num_css_critical_images());
}
TEST_F(CriticalImagesBeaconFilterTest, NoScriptInjectionWithNoScript) {
PrepareInjection();
GoogleString html = "<head></head><body><noscript>";
AddImageTags(&html);
StrAppend(&html, "</noscript></body>");
ParseUrl(GetTestUrl(), html);
VerifyNoInjection(0);
VerifyWithNoImageRewrite();
}
TEST_F(CriticalImagesBeaconFilterTest, DontRebeaconBeforeTimeout) {
RunInjection();
VerifyInjection(1);
VerifyWithNoImageRewrite();
// Write a dummy value to the property cache.
WriteToPropertyCache();
// No beacon injection happens on the immediately succeeding request.
ResetDriver();
SetDriverRequestHeaders();
SetupAndProcessUrl();
VerifyNoInjection(1);
// Beacon injection happens when the pcache value expires or when the
// reinstrumentation time interval is exceeded.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * 1000);
ResetDriver();
SetDriverRequestHeaders();
SetupAndProcessUrl();
VerifyInjection(2);
}
TEST_F(CriticalImagesBeaconFilterTest, BeaconReinstrumentationWithHeader) {
RunInjection();
VerifyInjection(1);
VerifyWithNoImageRewrite();
// Write a dummy value to the property cache.
WriteToPropertyCache();
// Beacon injection happens when the PS-ShouldBeacon header is present even
// when the pcache value has not expired and the reinstrumentation time
// interval has not been exceeded.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", "random_rebeaconing_key");
AddRequestAttribute(kPsaShouldBeacon, "random_rebeaconing_key");
SetDriverRequestHeaders();
SetupAndProcessUrl();
VerifyInjection(2);
}
TEST_F(CriticalImagesBeaconFilterTest, UnsupportedUserAgent) {
// Test that the filter is not applied for unsupported user agents.
SetCurrentUserAgent("Firefox/1.0");
RunInjection();
VerifyNoInjection(0);
}
TEST_F(CriticalImagesBeaconFilterTest, Googlebot) {
// Verify that the filter is not applied for bots.
SetCurrentUserAgent(UserAgentMatcherTestBase::kGooglebotUserAgent);
RunInjection();
VerifyNoInjection(0);
}
// Verify that the init string is set correctly to not run the beacon's onload
// handler when lazyload is enabled. The lazyload JS will take care of running
// the beacon when all images have been loaded.
TEST_F(CriticalImagesBeaconFilterTest, LazyloadEnabled) {
options()->EnableFilter(RewriteOptions::kLazyloadImages);
// On the first page access, there will be no critical image data and lazyload
// will be disabled.
RunInjection();
VerifyInjection(1);
// Advance time to force re-beaconing. Now there are extant non-critical
// images, and lazyload ought to be enabled.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * 1000);
ResetDriver();
SetupAndProcessUrl();
VerifyInjection(2);
}
} // namespace net_instaweb