text: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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.
   */
  
  
  #include "net/instaweb/rewriter/public/script_tag_scanner.h"
  
  #include <algorithm>
  
  #include "base/logging.h"
  #include "pagespeed/kernel/base/basictypes.h"
  #include "pagespeed/kernel/base/string.h"
  #include "pagespeed/kernel/base/string_util.h"
  #include "pagespeed/kernel/html/html_element.h"
  #include "pagespeed/kernel/html/html_name.h"
  
  namespace net_instaweb {
  
  // The list is from HTML5, "4.3.1.1 Scripting languages"
  class HtmlParse;
  
  static const char* const javascript_mimetypes[] = {
    "application/ecmascript",
    "application/javascript",
    "application/x-ecmascript",
    "application/x-javascript",
    "text/ecmascript",
    "text/javascript",
    "text/javascript1.0",
    "text/javascript1.1",
    "text/javascript1.2",
    "text/javascript1.3",
    "text/javascript1.4",
    "text/javascript1.5",
    "text/jscript",
    "text/livescript",
    "text/x-ecmascript",
    "text/x-javascript"
  };
  
  ScriptTagScanner::ScriptTagScanner(HtmlParse* html_parse) {
  #ifndef NDEBUG
    CharStarCompareSensitive less_than;
    for (int i = 0, n = arraysize(javascript_mimetypes); i < n - 1; ++i) {
      DCHECK(less_than(javascript_mimetypes[i], javascript_mimetypes[i + 1]));
    }
  #endif
  }
  
  ScriptTagScanner::ScriptClassification ScriptTagScanner::ParseScriptElement(
      HtmlElement* element, HtmlElement::Attribute** src) {
    if (element->keyword() != HtmlName::kScript) {
      return kNonScript;
    }
  
    *src = element->FindAttribute(HtmlName::kSrc);
  
    // Figure out if we have JS or not.
    // This is based on the 'type' and 'language' attributes, with
    // type having precedence. For this determination, however,
    // <script type> acts as if the type attribute is not there,
    // which is different from <script type="">
    ScriptClassification lang;
    HtmlElement::Attribute* type_attr = element->FindAttribute(HtmlName::kType);
    bool check_lang_attr = false;
    if (type_attr == NULL) {
      check_lang_attr = true;
    } else {
      StringPiece type_str = type_attr->DecodedValueOrNull();
      if (type_attr->decoding_error()) {
        lang = kUnknownScript;                 // e.g. <script type=&#257;>
      } else if (type_str.data() == NULL) {    // e.g. <script type>
        // If the type attribute is empty (no =) then fall back to the lang attr.
        check_lang_attr = true;
      } else if (type_str.empty() || IsJsMime(Normalized(type_str))) {
        // An empty type string (but not whitespace-only!) is JS,
        // So is one that's a known mimetype once lowercased and
        // having its leading and trailing whitespace removed
        lang = kJavaScript;
      } else {
        lang = kUnknownScript;
      }
    }
  
    // Without type= the ultra-deprecated language attribute determines
    // things.  empty or null one is ignored. The test is done
    // case-insensitively, but leading/trailing whitespace matters.
    // (Note: null check on ->DecodedValueOrNull() above as it's passed
    // to GoogleString)
    if (check_lang_attr) {
      HtmlElement::Attribute* lang_attr = element->FindAttribute(
          HtmlName::kLanguage);
  
      if (lang_attr != NULL) {
        StringPiece lang_piece = lang_attr->DecodedValueOrNull();
        if (lang_attr->decoding_error()) {
          lang = kUnknownScript;                 // e.g. <script language=&#257;>
        } else if (lang_piece.data() == NULL) {
          lang = kJavaScript;                    // e.g. <script language>
        } else {
          GoogleString lang_str;
          lang_piece.CopyToString(&lang_str);
          LowerString(&lang_str);
          if (lang_str.empty() || IsJsMime(StrCat("text/", lang_str))) {
            lang = kJavaScript;
          } else {
            lang = kUnknownScript;
          }
        }
      } else {
        // JS is the default if nothing is specified at all.
        lang = kJavaScript;
      }
    }
  
    return lang;
  }
  
  
  int ScriptTagScanner::ExecutionMode(const HtmlElement* element) const {
    int flags = 0;
  
    if (element->FindAttribute(HtmlName::kAsync) != NULL) {
      flags |= kExecuteAsync;
    }
  
    if (element->FindAttribute(HtmlName::kDefer) != NULL) {
      flags |= kExecuteDefer;
    }
  
    // HTML5 notes that certain values of IE-proprietary 'for' and 'event'
    // attributes are magic and are to be handled as if they're not there,
    // while others will cause the script to not be run at all.
    // Note: there is a disagreement between Chrome and Firefox on how
    // empty ones are handled. We set kExecuteForEvent as it is the conservative
    // value, requiring careful treatment by filters
    const HtmlElement::Attribute* for_attr = element->FindAttribute(
        HtmlName::kFor);
    const HtmlElement::Attribute* event_attr = element->FindAttribute(
        HtmlName::kEvent);
    if (for_attr != NULL && event_attr != NULL) {
      if (Normalized(for_attr->DecodedValueOrNull()) != "window") {
        flags |= kExecuteForEvent;
      }
      GoogleString event_str = Normalized(event_attr->DecodedValueOrNull());
      if (event_str != "onload" && event_str != "onload()") {
        flags |= kExecuteForEvent;
      }
    }
  
    return flags;
  }
  
  GoogleString ScriptTagScanner::Normalized(const StringPiece& str) {
    GoogleString normal_form;
    TrimWhitespace(str, &normal_form);
    LowerString(&normal_form);
    return normal_form;
  }
  
  bool ScriptTagScanner::IsJsMime(const GoogleString& type_str) {
    CharStarCompareSensitive less_than;  // case-folding done by caller.
    return std::binary_search(
        javascript_mimetypes,
        javascript_mimetypes + arraysize(javascript_mimetypes),
        type_str.c_str(),
        less_than);
  }
  
  }  // namespace net_instaweb
  