text: /*
   * 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 "base/logging.h"
  #include "net/instaweb/rewriter/public/critical_images_finder.h"
  #include "net/instaweb/rewriter/public/lazyload_images_filter.h"
  #include "net/instaweb/rewriter/public/request_properties.h"
  #include "net/instaweb/rewriter/public/rewrite_driver.h"
  #include "net/instaweb/rewriter/public/rewrite_options.h"
  #include "net/instaweb/rewriter/public/server_context.h"
  #include "net/instaweb/rewriter/public/static_asset_manager.h"
  #include "pagespeed/kernel/base/escaping.h"
  #include "pagespeed/kernel/base/hasher.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_element.h"
  #include "pagespeed/kernel/html/html_name.h"
  #include "pagespeed/kernel/http/google_url.h"
  #include "pagespeed/opt/logging/enums.pb.h"
  
  namespace net_instaweb {
  
  // Counters.
  const char CriticalImagesBeaconFilter::kCriticalImagesBeaconAddedCount[] =
      "critical_images_beacon_filter_script_added_count";
  
  // Onload code for img elements to detect whether they are critical or not.
  const char* CriticalImagesBeaconFilter::kImageOnloadCode =
      "pagespeed.CriticalImages.checkImageForCriticality(this);";
  
  CriticalImagesBeaconFilter::CriticalImagesBeaconFilter(RewriteDriver* driver)
      : CommonFilter(driver),
        added_beacon_js_(false) {
    Clear();
    Statistics* stats = driver->server_context()->statistics();
    critical_images_beacon_added_count_ = stats->GetVariable(
        kCriticalImagesBeaconAddedCount);
  }
  
  CriticalImagesBeaconFilter::~CriticalImagesBeaconFilter() {}
  
  bool CriticalImagesBeaconFilter::ShouldApply(RewriteDriver* driver) {
    // Default to not enabled.
    if (!driver->request_properties()->SupportsCriticalImagesBeacon()) {
      return false;
    }
    CriticalImagesFinder* finder =
        driver->server_context()->critical_images_finder();
    return finder->ShouldBeacon(driver);
  }
  
  void CriticalImagesBeaconFilter::DetermineEnabled(
      GoogleString* disabled_reason) {
    // We need the filter to be enabled to track the candidate images on the page,
    // even if we aren't actually inserting the beacon JS.
    set_is_enabled(true);
    // Make sure we don't have stray unused beacon metadata from a previous
    // document.  This has caught bugs in tests / during code modification where
    // the whole filter chain isn't run and cleaned up properly.
    DCHECK_EQ(kDoNotBeacon, beacon_metadata_.status);
    DCHECK(beacon_metadata_.nonce.empty());
    DCHECK(!insert_beacon_js_);
  
    if (driver()->request_properties()->SupportsCriticalImagesBeacon()) {
      // Check whether we need to beacon, and store the nonce we get.
      CriticalImagesFinder* finder =
          driver()->server_context()->critical_images_finder();
      beacon_metadata_ = finder->PrepareForBeaconInsertion(driver());
      insert_beacon_js_ = (beacon_metadata_.status != kDoNotBeacon);
    }
  }
  
  void CriticalImagesBeaconFilter::InitStats(Statistics* statistics) {
    statistics->AddVariable(kCriticalImagesBeaconAddedCount);
  }
  
  void CriticalImagesBeaconFilter::EndDocument() {
    CriticalImagesFinder* finder =
        driver()->server_context()->critical_images_finder();
    finder->UpdateCandidateImagesForBeaconing(image_url_hashes_, driver(),
                                              insert_beacon_js_);
    Clear();
  }
  
  void CriticalImagesBeaconFilter::MaybeAddBeaconJavascript(
      HtmlElement* element) {
    if (!insert_beacon_js_ || added_beacon_js_) {
      return;
    }
    added_beacon_js_ = true;
    StaticAssetManager* static_asset_manager =
        driver()->server_context()->static_asset_manager();
    GoogleString js = static_asset_manager->GetAsset(
        StaticAssetEnum::CRITICAL_IMAGES_BEACON_JS, driver()->options());
  
    // Create the init string to append at the end of the static JS.
    const RewriteOptions::BeaconUrl& beacons = driver()->options()->beacon_url();
    const GoogleString* beacon_url =
        driver()->IsHttps() ? &beacons.https : &beacons.http;
    GoogleString html_url;
    EscapeToJsStringLiteral(driver()->google_url().Spec(),
                            false, /* no quotes */
                            &html_url);
    GoogleString options_signature_hash =
        driver()->server_context()->hasher()->Hash(
            driver()->options()->signature());
    // If lazyload is enabled, it will run the beacon after it has loaded all the
    // images. Otherwise, run it at page onload.
    bool lazyload_will_beacon =
        driver()->options()->Enabled(RewriteOptions::kLazyloadImages) &&
        LazyloadImagesFilter::ShouldApply(driver()) ==
            RewriterHtmlApplication::ACTIVE;
    GoogleString send_beacon_at_onload = BoolToString(!lazyload_will_beacon);
    GoogleString resize_rendered_image_dimensions_enabled =
        BoolToString(driver()->options()->Enabled(
            RewriteOptions::kResizeToRenderedImageDimensions));
    StrAppend(&js,
              "\npagespeed.CriticalImages.Run('",
              *beacon_url, "','", html_url, "','",
              options_signature_hash, "',");
    StrAppend(&js, send_beacon_at_onload, ",",
              resize_rendered_image_dimensions_enabled, ",'",
              beacon_metadata_.nonce, "');");
    HtmlElement* script = driver()->NewElement(NULL, HtmlName::kScript);
    driver()->AddAttribute(script, HtmlName::kDataPagespeedNoDefer, NULL);
    // Always add the beacon js before the current node, because the current node
    // might be an img node that needs the beacon js for its
    // checkImageForCriticality onload handler.
    driver()->InsertNodeBeforeNode(element, script);
    AddJsToElement(js, script);
    critical_images_beacon_added_count_->Add(1);
  }
  
  void CriticalImagesBeaconFilter::Clear() {
    beacon_metadata_.status = kDoNotBeacon;
    beacon_metadata_.nonce.clear();
    image_url_hashes_.clear();
    insert_beacon_js_ = false;
    added_beacon_js_ = false;
  }
  
  void CriticalImagesBeaconFilter::EndElementImpl(HtmlElement* element) {
    if (element->keyword() != HtmlName::kImg &&
        element->keyword() != HtmlName::kInput) {
      return;
    }
    // TODO(jud): Verify this logic works correctly with input tags, then remove
    // the check for img tag here.
    if (element->keyword() == HtmlName::kImg && driver()->IsRewritable(element)) {
      // Add a data-pagespeed-url-hash attribute to the image with the hash of the
      // original URL. This is what the beacon will send back as the identifier
      // for critical images.
      HtmlElement::Attribute* src = element->FindAttribute(HtmlName::kSrc);
      if (src != NULL && src->DecodedValueOrNull() != NULL) {
        StringPiece url(src->DecodedValueOrNull());
        GoogleUrl gurl(driver()->base_url(), url);
        if (gurl.IsAnyValid()) {
          unsigned int hash_val = HashString<CasePreserve, unsigned int>(
              gurl.spec_c_str(), strlen(gurl.spec_c_str()));
          GoogleString hash_str = UintToString(hash_val);
          image_url_hashes_.insert(hash_str);
          if (insert_beacon_js_) {
            driver()->AddAttribute(
                element, HtmlName::kDataPagespeedUrlHash, hash_str);
            if (element->keyword() == HtmlName::kImg &&
                CanAddPagespeedOnloadToImage(*element)) {
              // Add an onload handler only if one is not already specified on the
              // non-rewritten page.
              driver()->AddAttribute(
                  element, HtmlName::kOnload, kImageOnloadCode);
              // TODO(sligocki): Should we add onerror handler here too?
              // If beacon javascript has not been added yet, we need to add it
              // before the current node because we are going to use the js for
              // the image criticality check on image-onload.
              MaybeAddBeaconJavascript(element);
            }
          }
        }
      }
    }
  }
  
  }  // namespace net_instaweb
  