| /* |
| * 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: sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/rewriter/public/css_filter.h" |
| |
| #include <algorithm> // for std::merge. |
| #include <map> |
| #include <utility> // for pair |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "net/instaweb/http/public/async_fetch.h" |
| #include "net/instaweb/http/public/log_record.h" |
| #include "net/instaweb/rewriter/cached_result.pb.h" |
| #include "net/instaweb/rewriter/public/association_transformer.h" |
| #include "net/instaweb/rewriter/public/css_absolutify.h" |
| #include "net/instaweb/rewriter/public/css_flatten_imports_context.h" |
| #include "net/instaweb/rewriter/public/css_hierarchy.h" |
| #include "net/instaweb/rewriter/public/css_image_rewriter.h" |
| #include "net/instaweb/rewriter/public/css_minify.h" |
| #include "net/instaweb/rewriter/public/css_tag_scanner.h" |
| #include "net/instaweb/rewriter/public/css_url_counter.h" |
| #include "net/instaweb/rewriter/public/css_util.h" |
| #include "net/instaweb/rewriter/public/data_url_input_resource.h" |
| #include "net/instaweb/rewriter/public/image_rewrite_filter.h" |
| #include "net/instaweb/rewriter/public/image_url_encoder.h" |
| #include "net/instaweb/rewriter/public/inline_attribute_slot.h" |
| #include "net/instaweb/rewriter/public/inline_output_resource.h" |
| #include "net/instaweb/rewriter/public/inline_resource_slot.h" |
| #include "net/instaweb/rewriter/public/output_resource.h" |
| #include "net/instaweb/rewriter/public/request_properties.h" |
| #include "net/instaweb/rewriter/public/resource.h" |
| #include "net/instaweb/rewriter/public/resource_slot.h" |
| #include "net/instaweb/rewriter/public/resource_tag_scanner.h" |
| #include "net/instaweb/rewriter/public/rewrite_context.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/rewrite_result.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "net/instaweb/rewriter/public/single_rewrite_context.h" |
| #include "net/instaweb/rewriter/public/usage_data_reporter.h" |
| #include "pagespeed/kernel/base/basictypes.h" |
| #include "pagespeed/kernel/base/charset_util.h" |
| #include "pagespeed/kernel/base/hasher.h" |
| #include "pagespeed/kernel/base/message_handler.h" |
| #include "pagespeed/kernel/base/scoped_ptr.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/base/string_writer.h" |
| #include "pagespeed/kernel/html/html_element.h" |
| #include "pagespeed/kernel/html/html_name.h" |
| #include "pagespeed/kernel/html/html_node.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/data_url.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| #include "pagespeed/kernel/util/simple_random.h" |
| #include "pagespeed/opt/logging/enums.pb.h" |
| #include "webutil/css/parser.h" |
| |
| #include "base/at_exit.h" |
| |
| namespace { |
| |
| base::AtExitManager* at_exit_manager = NULL; |
| |
| } // namespace |
| |
| namespace net_instaweb { |
| |
| class CacheExtender; |
| class ImageCombineFilter; |
| |
| namespace { |
| |
| // A simple transformer that resolves URLs against a base. Unlike |
| // RewriteDomainTransformer, does not do any mapping or trimming. |
| class SimpleAbsolutifyTransformer : public CssTagScanner::Transformer { |
| public: |
| explicit SimpleAbsolutifyTransformer(const GoogleUrl* base_url) |
| : base_url_(base_url) {} |
| virtual ~SimpleAbsolutifyTransformer() {} |
| |
| virtual TransformStatus Transform(GoogleString* str) { |
| GoogleUrl abs(*base_url_, *str); |
| if (abs.IsWebValid()) { |
| abs.Spec().CopyToString(str); |
| return kSuccess; |
| } else { |
| return kNoChange; |
| } |
| } |
| |
| private: |
| const GoogleUrl* base_url_; |
| DISALLOW_COPY_AND_ASSIGN(SimpleAbsolutifyTransformer); |
| }; |
| |
| // All of the options that can affect image optimization can also affect |
| // CSS rewriting, due to embedded images. We will merge those in during |
| // Initialize. There are additional options that affect CSS files. Notably, |
| // image inlining does not affect the http* URLs of images, but it does affect |
| // the URLs of CSS files because images inlined into CSS changes the hash. |
| const RewriteOptions::Filter kRelatedFilters[] = { |
| RewriteOptions::kExtendCacheCss, |
| RewriteOptions::kExtendCacheImages, |
| RewriteOptions::kFallbackRewriteCssUrls, |
| RewriteOptions::kFlattenCssImports, |
| RewriteOptions::kInlineImages, |
| RewriteOptions::kLeftTrimUrls, |
| RewriteOptions::kRewriteDomains, |
| RewriteOptions::kSpriteImages, |
| }; |
| const int kRelatedFiltersSize = arraysize(kRelatedFilters); |
| |
| const char* const kRelatedOptions[] = { |
| RewriteOptions::kCssFlattenMaxBytes, |
| RewriteOptions::kCssImageInlineMaxBytes, |
| RewriteOptions::kCssPreserveURLs, |
| RewriteOptions::kImagePreserveURLs, |
| RewriteOptions::kMaxUrlSegmentSize, |
| RewriteOptions::kMaxUrlSize, |
| }; |
| |
| bool IsInlineResource(const ResourcePtr& resource) { |
| // InlineOutputResources have no URL, but original inline resources are |
| // stored as DataUrlInputResources, thus have data url() |
| // TODO(sligocki): Harmonize these all to use the same method. |
| return (!resource->has_url() || IsDataUrl(resource->url())); |
| } |
| |
| } // namespace |
| |
| const RewriteOptions::Filter* CssFilter::merged_filters_ = NULL; |
| int CssFilter::merged_filters_size_ = 0; |
| |
| StringPieceVector* CssFilter::related_options_ = NULL; |
| |
| // Statistics variable names. |
| const char CssFilter::kBlocksRewritten[] = "css_filter_blocks_rewritten"; |
| const char CssFilter::kParseFailures[] = "css_filter_parse_failures"; |
| const char CssFilter::kFallbackRewrites[] = "css_filter_fallback_rewrites"; |
| const char CssFilter::kFallbackFailures[] = "css_filter_fallback_failures"; |
| const char CssFilter::kRewritesDropped[] = "css_filter_rewrites_dropped"; |
| const char CssFilter::kTotalBytesSaved[] = "css_filter_total_bytes_saved"; |
| const char CssFilter::kTotalOriginalBytes[] = "css_filter_total_original_bytes"; |
| const char CssFilter::kUses[] = "css_filter_uses"; |
| const char CssFilter::kCharsetMismatch[] = "flatten_imports_charset_mismatch"; |
| const char CssFilter::kInvalidUrl[] = "flatten_imports_invalid_url"; |
| const char CssFilter::kLimitExceeded[] = "flatten_imports_limit_exceeded"; |
| const char CssFilter::kMinifyFailed[] = "flatten_imports_minify_failed"; |
| const char CssFilter::kRecursion[] = "flatten_imports_recursion"; |
| const char CssFilter::kComplexQueries[] = "flatten_imports_complex_queries"; |
| |
| CssFilter::Context::Context(CssFilter* filter, RewriteDriver* driver, |
| RewriteContext* parent, |
| CacheExtender* cache_extender, |
| ImageRewriteFilter* image_rewriter, |
| ImageCombineFilter* image_combiner, |
| ResourceContext* context) |
| : SingleRewriteContext(driver, parent, context), |
| filter_(filter), |
| css_image_rewriter_( |
| new CssImageRewriter(this, filter, |
| cache_extender, image_rewriter, |
| image_combiner)), |
| image_rewrite_filter_(image_rewriter), |
| hierarchy_(filter), |
| css_rewritten_(false), |
| has_utf8_bom_(false), |
| fallback_mode_(false), |
| rewrite_element_(NULL), |
| rewrite_inline_element_(NULL), |
| rewrite_inline_char_node_(NULL), |
| rewrite_inline_attribute_(NULL), |
| rewrite_inline_css_kind_(kInsideStyleTag), |
| in_text_size_(-1) { |
| initial_css_base_gurl_.Reset(filter_->decoded_base_url()); |
| DCHECK(initial_css_base_gurl_.IsWebValid()); |
| initial_css_trim_gurl_.Reset(initial_css_base_gurl_); |
| } |
| |
| CssFilter::Context::~Context() { |
| } |
| |
| // The base URL used when absolutifying sub-resources must be the input |
| // URL of this rewrite. |
| // |
| // The only exception is the case of inline CSS, where we define the |
| // input URL to be a data: URL. In this case the base URL is the URL of |
| // the HTML page, which we save to initial_... in the constructor. |
| // |
| // When our input is the output of CssCombiner, the initial_css_base_gurl_ here |
| // is stale (it's the first input to the combination). It ought to be |
| // the URL of the output of the combination. Similarly css_trim_gurl |
| // needs to be set from the ultimate output resource and not just |
| // initial_css_trim_gurl_. This matters because for a cross-directory |
| // combine we can end up moving a few directories up, and further a UrlNamer |
| // might even end up moving some things to a separate cookieless domain. |
| // |
| // Note that we have to do it functionally and not in RewriteSingle since these |
| // may be invoked from Absolutify, which may be invoked from a |
| // different thread when doing fallback due to a deadline. This also means that |
| // initial_css_base_gurl_ and initial_css_trim_gurl_ must indeed just be |
| // initials and not be mutated. |
| void CssFilter::Context::GetCssBaseUrlToUse( |
| const ResourcePtr& input_resource, GoogleUrl* css_base_gurl_to_use) { |
| if (!IsInlineResource(input_resource)) { |
| css_base_gurl_to_use->Reset(input_resource->url()); |
| } else { |
| css_base_gurl_to_use->Reset(initial_css_base_gurl_); |
| } |
| } |
| |
| void CssFilter::Context::GetCssTrimUrlToUse( |
| const ResourcePtr& input_resource, |
| const StringPiece& output_url_base, |
| GoogleUrl* css_trim_gurl_to_use) { |
| if (!IsInlineResource(input_resource)) { |
| css_trim_gurl_to_use->Reset(output_url_base); |
| } else { |
| css_trim_gurl_to_use->Reset(initial_css_trim_gurl_); |
| } |
| } |
| |
| void CssFilter::Context::GetCssTrimUrlToUse( |
| const ResourcePtr& input_resource, |
| const OutputResourcePtr& output_resource, |
| GoogleUrl* css_trim_gurl_to_use) { |
| if (!IsInlineResource(input_resource)) { |
| css_trim_gurl_to_use->Reset(output_resource->UrlEvenIfHashNotSet()); |
| } else { |
| css_trim_gurl_to_use->Reset(initial_css_trim_gurl_); |
| } |
| } |
| |
| bool CssFilter::Context::SendFallbackResponse( |
| StringPiece output_url_base, |
| StringPiece input_contents, |
| AsyncFetch* async_fetch, |
| MessageHandler* handler) { |
| // Do not set the content length, since we may need to mutate the |
| // content as we stream out the bytes to correct for URL changes. |
| async_fetch->HeadersComplete(); |
| |
| DCHECK_EQ(1, num_slots()); |
| ResourcePtr input_resource(slot(0)->resource()); |
| DCHECK(input_resource.get() != NULL); |
| |
| GoogleUrl css_base_gurl_to_use; |
| GetCssBaseUrlToUse(input_resource, &css_base_gurl_to_use); |
| |
| GoogleUrl css_trim_gurl_to_use; |
| GetCssTrimUrlToUse(input_resource, output_url_base, &css_trim_gurl_to_use); |
| |
| bool ret = false; |
| switch (Driver()->ResolveCssUrls(css_base_gurl_to_use, |
| css_trim_gurl_to_use.Spec(), |
| input_contents, async_fetch, handler)) { |
| case RewriteDriver::kNoResolutionNeeded: |
| case RewriteDriver::kWriteFailed: |
| // If kNoResolutionNeeded, we just write out the input_contents, because |
| // nothing needed to be changed. |
| // |
| // If kWriteFailed, this means that the URLs couldn't be transformed |
| // (or that writer->Write() actually failed ... I think this shouldn't |
| // generally happen). So, we just push out the unedited original, |
| // figuring that must be better than nothing. |
| // |
| // TODO(sligocki): In the fetch path ResolveCssUrls should never fail |
| // to transform URLs. We should just absolutify all the ones we can. |
| ret = async_fetch->Write(input_contents, handler); |
| break; |
| case RewriteDriver::kSuccess: |
| ret = true; |
| break; |
| } |
| return ret; |
| } |
| |
| void CssFilter::Context::Render() { |
| if (num_output_partitions() == 0) { |
| return; |
| } |
| |
| DCHECK(has_parent() || (rewrite_element_ != NULL)); |
| |
| const CachedResult& result = *output_partition(0); |
| if (result.optimizable()) { |
| // Note: All actual rendering is done inside ResourceSlot::Render() methods. |
| if (rewrite_inline_char_node_ == NULL && |
| rewrite_inline_attribute_ == NULL) { |
| // External css. |
| Driver()->log_record()->SetRewriterLoggingStatus( |
| id(), slot(0)->resource()->url(), RewriterApplication::APPLIED_OK); |
| } |
| filter_->num_uses_->Add(1); |
| } |
| |
| if (Driver()->options()->Enabled( |
| RewriteOptions::kExperimentCollectMobImageInfo) && |
| !has_parent() /* only report at top-level*/) { |
| for (int i = 0; i < result.associated_image_info_size(); ++i) { |
| image_rewrite_filter_->RegisterImageInfo(result.associated_image_info(i)); |
| } |
| } |
| } |
| |
| void CssFilter::Context::SetupInlineRewrite(HtmlElement* style_element, |
| HtmlCharactersNode* text) { |
| // To handle nested rewrites of inline CSS, we internally handle it |
| // as a rewrite of a data: URL. |
| rewrite_element_ = style_element; |
| rewrite_inline_element_ = style_element; |
| rewrite_inline_char_node_ = text; |
| rewrite_inline_css_kind_ = kInsideStyleTag; |
| } |
| |
| void CssFilter::Context::SetupAttributeRewrite(HtmlElement* element, |
| HtmlElement::Attribute* src, |
| InlineCssKind inline_css_kind) { |
| DCHECK(inline_css_kind == kAttributeWithoutUrls || |
| inline_css_kind == kAttributeWithUrls); |
| rewrite_element_ = element; |
| rewrite_inline_element_ = element; |
| rewrite_inline_attribute_ = src; |
| rewrite_inline_css_kind_ = inline_css_kind; |
| } |
| |
| void CssFilter::Context::SetupExternalRewrite(HtmlElement* element, |
| const GoogleUrl& base_gurl, |
| const GoogleUrl& trim_gurl) { |
| rewrite_element_ = element; |
| initial_css_base_gurl_.Reset(base_gurl); |
| initial_css_trim_gurl_.Reset(trim_gurl); |
| } |
| |
| void CssFilter::Context::RewriteSingle( |
| const ResourcePtr& input_resource, |
| const OutputResourcePtr& output_resource) { |
| int drop_percentage = Options()->rewrite_random_drop_percentage(); |
| if (drop_percentage > 0) { |
| SimpleRandom* simple_random = FindServerContext()->simple_random(); |
| if (drop_percentage > static_cast<int>(simple_random->Next() % 100)) { |
| return RewriteDone(kTooBusy, 0); |
| } |
| } |
| |
| bool is_ipro = IsNestedIn(RewriteOptions::kInPlaceRewriteId); |
| AttachDependentRequestTrace(is_ipro ? "IproProcessCSS" : "ProcessCSS"); |
| input_resource_ = input_resource; |
| output_resource_ = output_resource; |
| StringPiece input_contents = input_resource_->ExtractUncompressedContents(); |
| in_text_size_ = input_contents.size(); |
| has_utf8_bom_ = StripUtf8Bom(&input_contents); |
| |
| GoogleUrl css_base_gurl_to_use; |
| GetCssBaseUrlToUse(input_resource, &css_base_gurl_to_use); |
| GoogleUrl css_trim_gurl_to_use; |
| GetCssTrimUrlToUse(input_resource, output_resource_, &css_trim_gurl_to_use); |
| bool parsed = RewriteCssText( |
| css_base_gurl_to_use, css_trim_gurl_to_use, input_contents, in_text_size_, |
| IsInlineAttribute() /* text_is_declarations */, |
| Driver()->message_handler()); |
| |
| if (parsed) { |
| if (num_nested() > 0) { |
| StartNestedTasks(); |
| } else { |
| // We call Harvest() ourselves so we can centralize all the output there. |
| Harvest(); |
| } |
| } else { |
| RewriteDone(kRewriteFailed, 0); |
| } |
| } |
| |
| // Return value answers the question: May we rewrite? |
| // css_base_gurl is the URL used to resolve relative URLs in the CSS. |
| // css_trim_gurl is the URL used to trim absolute URLs to relative URLs. |
| // Specifically, it should be the address of the CSS document itself for |
| // external CSS or the HTML document that the CSS is in for inline CSS. |
| bool CssFilter::Context::RewriteCssText(const GoogleUrl& css_base_gurl, |
| const GoogleUrl& css_trim_gurl, |
| const StringPiece& in_text, |
| int64 in_text_size, |
| bool text_is_declarations, |
| MessageHandler* handler) { |
| // Load stylesheet w/o expanding background attributes and preserving as |
| // much content as possible from the original document. |
| Css::Parser parser(in_text); |
| parser.set_preservation_mode(true); |
| // We avoid quirks-mode so that we do not "fix" something we shouldn't have. |
| parser.set_quirks_mode(false); |
| // Create a stylesheet even if given declarations so that we don't need |
| // two versions of everything, though they do need to handle a stylesheet |
| // with no selectors in it, which they currently do. |
| scoped_ptr<Css::Stylesheet> stylesheet; |
| if (text_is_declarations) { |
| Css::Declarations* declarations = parser.ParseRawDeclarations(); |
| if (declarations != NULL) { |
| stylesheet.reset(new Css::Stylesheet()); |
| Css::Ruleset* ruleset = new Css::Ruleset(); |
| stylesheet->mutable_rulesets().push_back(ruleset); |
| ruleset->set_declarations(declarations); |
| } |
| } else { |
| stylesheet.reset(parser.ParseRawStylesheet()); |
| } |
| |
| bool parsed = true; |
| if (stylesheet.get() == NULL || |
| parser.errors_seen_mask() != Css::Parser::kNoError) { |
| parsed = false; |
| Driver()->message_handler()->Message( |
| kWarning, "CSS parsing error in %s", css_base_gurl.spec_c_str()); |
| filter_->num_parse_failures_->Add(1); |
| |
| // Report all parse errors (Note: Some of these are errors we recovered |
| // from by passing through unparsed sections of text). |
| for (int i = 0, n = parser.errors_seen().size(); i < n; ++i) { |
| Css::Parser::ErrorInfo error = parser.errors_seen()[i]; |
| Driver()->server_context()->usage_data_reporter()->ReportWarning( |
| css_base_gurl, error.error_num, error.message); |
| } |
| |
| // TODO(sligocki): Do we want to add the actual parse errors to this |
| // comment? There are often a lot and they can be quite long, so I'm not |
| // sure it's the best idea. Perhaps better to ask users to use the command |
| // line utility? Or is it better to give them all the info in one place? |
| output_partition(0)->add_debug_message(StrCat( |
| "CSS rewrite failed: Parse error in ", css_base_gurl.Spec())); |
| } else { |
| // Edit stylesheet. |
| // Any problem with an @import results in the error mask bit kImportError |
| // being set, so if we get here we know that any @import rules were parsed |
| // successfully, thus, flattening is safe. |
| bool has_unparseables = (parser.unparseable_sections_seen_mask() != |
| Css::Parser::kNoError); |
| RewriteCssFromRoot(css_base_gurl, css_trim_gurl, in_text, in_text_size, |
| has_unparseables, stylesheet.release()); |
| } |
| |
| if (!parsed && |
| Driver()->options()->Enabled(RewriteOptions::kFallbackRewriteCssUrls)) { |
| parsed = FallbackRewriteUrls(css_base_gurl, css_trim_gurl, in_text); |
| } |
| |
| return parsed; |
| } |
| |
| void CssFilter::Context::RewriteCssFromRoot(const GoogleUrl& css_base_gurl, |
| const GoogleUrl& css_trim_gurl, |
| const StringPiece& contents, |
| int64 in_text_size, |
| bool has_unparseables, |
| Css::Stylesheet* stylesheet) { |
| DCHECK_EQ(in_text_size_, in_text_size); |
| |
| hierarchy_.InitializeRoot(css_base_gurl, css_trim_gurl, |
| contents, has_unparseables, |
| Driver()->options()->css_flatten_max_bytes(), |
| stylesheet, Driver()->message_handler()); |
| |
| css_rewritten_ = css_image_rewriter_->RewriteCss(ImageInlineMaxBytes(), |
| this, |
| &hierarchy_, |
| Driver()->message_handler()); |
| } |
| |
| void CssFilter::Context::RewriteCssFromNested(RewriteContext* parent, |
| CssHierarchy* hierarchy) { |
| css_image_rewriter_->RewriteCss(ImageInlineMaxBytes(), parent, hierarchy, |
| Driver()->message_handler()); |
| } |
| |
| // Fallback to rewriting URLs using CssTagScanner because of failure to parse. |
| // Note: We do not flatten CSS during fallback processing. |
| // TODO(sligocki): Allow recursive rewriting of @imported CSS files. |
| bool CssFilter::Context::FallbackRewriteUrls( |
| const GoogleUrl& css_base_gurl, const GoogleUrl& css_trim_gurl, |
| const StringPiece& in_text) { |
| fallback_mode_ = true; |
| |
| // We need permanent copies of these since fallback transformers |
| // keep pointers. |
| base_gurl_for_fallback_.reset(new GoogleUrl()); |
| base_gurl_for_fallback_->Reset(css_base_gurl); |
| trim_gurl_for_fallback_.reset(new GoogleUrl()); |
| trim_gurl_for_fallback_->Reset(css_trim_gurl); |
| |
| bool ret = false; |
| // In order to rewrite CSS using only the CssTagScanner, we run two scans. |
| // Here we just record all URLs found with the CssUrlCounter. |
| // The second run will be in Harvest() after all the subresources have been |
| // rewritten. |
| CssUrlCounter url_counter(&css_base_gurl, Driver()->message_handler()); |
| if (url_counter.Count(in_text)) { |
| // TransformUrls will succeed only if all the URLs in the CSS file |
| // were parseable. If we encounter any unparseable URLs, we will not |
| // be able to absolutify them and so should not rewrite the CSS. |
| ret = true; |
| |
| // Setup absolutifier used by fallback_transformer_. Only enable it if |
| // we need to absolutify resources. Otherwise leave it as NULL. |
| bool proxy_mode; |
| RewriteDriver* driver = Driver(); |
| if (driver->ShouldAbsolutifyUrl(css_base_gurl, css_trim_gurl, |
| &proxy_mode)) { |
| absolutifier_.reset(new RewriteDomainTransformer( |
| base_gurl_for_fallback_.get(), trim_gurl_for_fallback_.get(), |
| driver->server_context(), driver->options(), |
| driver->message_handler())); |
| if (proxy_mode) { |
| absolutifier_->set_trim_urls(false); |
| } |
| } |
| // fallback_transformer_ will be used in the second pass (in Harvest()) |
| // to rewrite the URLs. |
| // We instantiate it here so that all the slots below can be set to render |
| // into it. When they are rendered they will set the map used by |
| // AssociationTransformer. |
| fallback_transformer_.reset(new AssociationTransformer( |
| base_gurl_for_fallback_.get(), driver->options(), absolutifier_.get(), |
| driver->message_handler())); |
| |
| const StringIntMap& url_counts = url_counter.url_counts(); |
| for (StringIntMap::const_iterator it = url_counts.begin(); |
| it != url_counts.end(); ++it) { |
| const GoogleUrl url(it->first); |
| // TODO(sligocki): Use count of occurrences to decide which URLs to |
| // inline. it->second has the count of how many occurrences of this |
| // URL there were. |
| // This is guaranteed by CssUrlCounter. |
| CHECK(url.IsAnyValid()) << it->first; |
| // Add slot. |
| bool is_authorized; |
| ResourcePtr resource = Driver()->CreateInputResource(url, &is_authorized); |
| if (resource.get()) { |
| ResourceSlotPtr slot(new AssociationSlot( |
| resource, fallback_transformer_->map(), url.Spec())); |
| css_image_rewriter_->RewriteSlot(slot, ImageInlineMaxBytes(), this); |
| } else if (!is_authorized) { |
| output_partition(0)->add_debug_message( |
| StrCat("A resource was not rewritten because ", url.Host(), |
| " is not an authorized domain")); |
| } |
| } |
| } |
| return ret; |
| } |
| |
| void CssFilter::Context::Harvest() { |
| GoogleString out_text; |
| bool ok = false; |
| |
| // Propagate any info on images from child rewrites. |
| CssImageRewriter::InheritChildImageInfo(this); |
| |
| if (fallback_mode_) { |
| // If CSS was not successfully parsed. |
| if (fallback_transformer_.get() != NULL) { |
| StringWriter out(&out_text); |
| ok = CssTagScanner::TransformUrls( |
| input_resource_->ExtractUncompressedContents(), &out, |
| fallback_transformer_.get(), Driver()->message_handler()); |
| } |
| if (ok) { |
| filter_->num_fallback_rewrites_->Add(1); |
| } else { |
| filter_->num_fallback_failures_->Add(1); |
| GoogleUrl css_base_gurl; |
| GetCssBaseUrlToUse(input_resource_, &css_base_gurl); |
| output_partition(0)->add_debug_message(StrCat( |
| "CSS rewrite failed: Fallback transformer error in ", |
| css_base_gurl.Spec())); |
| } |
| |
| } else { |
| // If we are limiting the size of the flattened result, work that out now; |
| // simply rolling up the contents does that nicely. |
| if (hierarchy_.flattening_succeeded() && |
| hierarchy_.flattened_result_limit() > 0) { |
| hierarchy_.RollUpContents(); |
| } |
| |
| // If CSS was successfully parsed. |
| hierarchy_.RollUpStylesheets(); |
| |
| bool previously_optimized = false; |
| for (int i = 0; !previously_optimized && i < num_nested(); ++i) { |
| RewriteContext* nested_context = nested(i); |
| for (int j = 0; j < nested_context->num_slots(); ++j) { |
| if (nested_context->slot(j)->was_optimized()) { |
| previously_optimized = true; |
| break; |
| } |
| } |
| } |
| |
| GoogleUrl css_base_gurl_to_use; |
| GetCssBaseUrlToUse(input_resource_, &css_base_gurl_to_use); |
| |
| GoogleUrl css_trim_gurl_to_use; |
| GetCssTrimUrlToUse(input_resource_, output_resource_, |
| &css_trim_gurl_to_use); |
| |
| // May need to absolutify @import and/or url() URLs. Note we must invoke |
| // ShouldAbsolutifyUrl first because we need 'proxying' to be calculated. |
| bool absolutified_urls = false; |
| bool proxying = false; |
| bool should_absolutify = Driver()->ShouldAbsolutifyUrl(css_base_gurl_to_use, |
| css_trim_gurl_to_use, |
| &proxying); |
| if (should_absolutify) { |
| absolutified_urls = |
| CssAbsolutify::AbsolutifyImports(hierarchy_.mutable_stylesheet(), |
| css_base_gurl_to_use); |
| } |
| |
| // If we have determined that we need to absolutify URLs, or if we are |
| // proxying (*), we need to absolutify all URLs. If we have already run the |
| // CSS through the image rewriter then all parseable URLs have already been |
| // done, and we only need to do unparseable URLs if any were detected. |
| // (*) When proxying the root of the path can change so we need to |
| // absolutify. |
| if (should_absolutify || proxying) { |
| absolutified_urls |= CssAbsolutify::AbsolutifyUrls( |
| hierarchy_.mutable_stylesheet(), |
| css_base_gurl_to_use, |
| !css_rewritten_, /* handle_parseable_ruleset_sections */ |
| hierarchy_.unparseable_detected(), /* handle_unparseable_sections */ |
| Driver(), |
| Driver()->message_handler()); |
| } |
| |
| ok = SerializeCss( |
| in_text_size_, hierarchy_.mutable_stylesheet(), css_base_gurl_to_use, |
| css_trim_gurl_to_use, previously_optimized || absolutified_urls, |
| IsInlineAttribute() /* stylesheet_is_declarations */, has_utf8_bom_, |
| &out_text, Driver()->message_handler()); |
| } |
| |
| if (ok) { |
| if (rewrite_inline_element_ == NULL) { |
| ServerContext* server_context = FindServerContext(); |
| server_context->MergeNonCachingResponseHeaders(input_resource_, |
| output_resource_); |
| } else { |
| output_partition(0)->set_inlined_data(out_text); |
| output_partition(0)->set_is_inline_output_resource(true); |
| } |
| ok = Driver()->Write(ResourceVector(1, input_resource_), |
| out_text, |
| &kContentTypeCss, |
| input_resource_->charset(), |
| output_resource_.get()); |
| } |
| |
| if (!hierarchy_.flattening_failure_reason().empty()) { |
| output_partition(0)->add_debug_message( |
| hierarchy_.flattening_failure_reason()); |
| } |
| |
| if (ok) { |
| RewriteDone(kRewriteOk, 0); |
| } else { |
| RewriteDone(kRewriteFailed, 0); |
| } |
| } |
| |
| bool CssFilter::Context::SerializeCss(int64 in_text_size, |
| const Css::Stylesheet* stylesheet, |
| const GoogleUrl& css_base_gurl, |
| const GoogleUrl& css_trim_gurl, |
| bool previously_optimized, |
| bool stylesheet_is_declarations, |
| bool add_utf8_bom, |
| GoogleString* out_text, |
| MessageHandler* handler) { |
| bool ret = true; |
| |
| // Re-serialize stylesheet. |
| StringWriter writer(out_text); |
| if (add_utf8_bom) { |
| writer.Write(kUtf8Bom, handler); |
| } |
| if (stylesheet_is_declarations) { |
| CHECK_EQ(Css::Ruleset::RULESET, stylesheet->ruleset(0).type()); |
| CssMinify::Declarations(stylesheet->ruleset(0).declarations(), |
| &writer, handler); |
| } else { |
| CssMinify::Stylesheet(*stylesheet, &writer, handler); |
| } |
| |
| // Get signed versions so that we can subtract them. |
| int64 out_text_size = static_cast<int64>(out_text->size()); |
| int64 bytes_saved = in_text_size - out_text_size; |
| |
| if (!Driver()->options()->always_rewrite_css()) { |
| // Don't rewrite if we didn't edit it or make it any smaller. |
| if (!previously_optimized && bytes_saved <= 0) { |
| ret = false; |
| Driver()->InfoAt(this, "CSS parser increased size of CSS file %s by %s " |
| "bytes.", css_base_gurl.spec_c_str(), |
| Integer64ToString(-bytes_saved).c_str()); |
| filter_->num_rewrites_dropped_->Add(1); |
| output_partition(0)->add_debug_message(StrCat( |
| "CSS rewrite failed: Cannot improve ", css_base_gurl.Spec())); |
| } |
| } |
| |
| // Statistics |
| if (ret) { |
| filter_->num_blocks_rewritten_->Add(1); |
| filter_->total_bytes_saved_->Add(bytes_saved); |
| // TODO(sligocki): Will this be misleading if we flatten @imports? |
| filter_->total_original_bytes_->Add(in_text_size); |
| } |
| return ret; |
| } |
| |
| int64 CssFilter::Context::ImageInlineMaxBytes() const { |
| if (rewrite_inline_element_ != NULL) { |
| // We're in an html context. |
| return std::min( |
| Driver()->options()->ImageInlineMaxBytes(), |
| Driver()->options()->CssImageInlineMaxBytes()); |
| } else { |
| // We're in a standalone CSS file. |
| return Driver()->options()->CssImageInlineMaxBytes(); |
| } |
| } |
| |
| bool CssFilter::Context::Partition(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| if (rewrite_inline_element_ == NULL) { |
| return SingleRewriteContext::Partition(partitions, outputs); |
| } else { |
| // We use kOmitInputHash here as this is for inline content. |
| CachedResult* partition = partitions->add_partition(); |
| slot(0)->resource()->AddInputInfoToPartition( |
| Resource::kOmitInputHash, 0, partition); |
| OutputResourcePtr output_resource( |
| InlineOutputResource::MakeInlineOutputResource(Driver())); |
| output_resource->set_cached_result(partition); |
| outputs->push_back(output_resource); |
| return true; |
| } |
| } |
| |
| GoogleString CssFilter::Context::UserAgentCacheKey( |
| const ResourceContext* resource_context) const { |
| GoogleString key; |
| if (resource_context != NULL) { |
| // CSS cache-key is sensitive to whether the UA supports webp or not. |
| key = ImageUrlEncoder::CacheKeyFromResourceContext(*resource_context); |
| } |
| // The cache key we get from the image codec is not sufficient, as |
| // it does not produce different results if CSS image inlining is |
| // on, but of course the css rewriter does. |
| if ((Options()->CssImageInlineMaxBytes() != 0) && |
| Driver()->request_properties()->SupportsImageInlining()) { |
| StrAppend(&key, "I"); |
| } else { |
| StrAppend(&key, "A"); |
| } |
| return key; |
| } |
| |
| GoogleString CssFilter::Context::CacheKeySuffix() const { |
| GoogleString suffix; |
| if (rewrite_inline_element_ != NULL) { |
| // Incorporate the base path of the HTML as part of the key --- it |
| // matters for inline CSS since resources are resolved against |
| // that (while it doesn't for external CSS, since that uses the |
| // stylesheet as the base). |
| switch (rewrite_inline_css_kind_) { |
| case kInsideStyleTag: { |
| const Hasher* hasher = FindServerContext()->lock_hasher(); |
| StrAppend(&suffix, "_@", |
| hasher->Hash(initial_css_base_gurl_.AllExceptLeaf())); |
| break; |
| } |
| |
| case kAttributeWithUrls: { |
| // For attributes, we take a somewhat different strategy. There are |
| // a lot of them, and they can be repeated in many directories, |
| // so just appending the directory causes the metadata cache usage |
| // to balloon. Fortunately, they are also usually very short, |
| // so instead, we use the absolutified version of the data: URLs |
| // as a disambiguator, so that paths that resolve URLs the same way |
| // get the same keys. |
| GoogleString absolutified_version; |
| SimpleAbsolutifyTransformer transformer(&Driver()->decoded_base_url()); |
| StringWriter writer(&absolutified_version); |
| CssTagScanner::TransformUrls( |
| slot(0)->resource()->ExtractUncompressedContents(), &writer, |
| &transformer, Driver()->message_handler()); |
| |
| const Hasher* hasher = FindServerContext()->lock_hasher(); |
| StrAppend(&suffix, "_@", hasher->Hash(absolutified_version)); |
| break; |
| } |
| |
| case kAttributeWithoutUrls: { |
| // If there are no URLs, then there is no dependence on the |
| // path, either. |
| break; |
| } |
| } |
| } |
| |
| return suffix; |
| } |
| |
| CssFilter::CssFilter(RewriteDriver* driver, |
| CacheExtender* cache_extender, |
| ImageRewriteFilter* image_rewriter, |
| ImageCombineFilter* image_combiner) |
| : RewriteFilter(driver), |
| in_style_element_(false), |
| style_element_(NULL), |
| cache_extender_(cache_extender), |
| image_rewrite_filter_(image_rewriter), |
| image_combiner_(image_combiner) { |
| Statistics* stats = server_context()->statistics(); |
| num_blocks_rewritten_ = stats->GetVariable(CssFilter::kBlocksRewritten); |
| num_parse_failures_ = stats->GetVariable(CssFilter::kParseFailures); |
| num_fallback_rewrites_ = stats->GetVariable(CssFilter::kFallbackRewrites); |
| num_fallback_failures_ = stats->GetVariable(CssFilter::kFallbackFailures); |
| num_rewrites_dropped_ = stats->GetVariable(CssFilter::kRewritesDropped); |
| total_bytes_saved_ = stats->GetUpDownCounter(CssFilter::kTotalBytesSaved); |
| total_original_bytes_ = stats->GetVariable(CssFilter::kTotalOriginalBytes); |
| num_uses_ = stats->GetVariable(CssFilter::kUses); |
| num_flatten_imports_charset_mismatch_ = stats->GetVariable(kCharsetMismatch); |
| num_flatten_imports_invalid_url_ = stats->GetVariable(kInvalidUrl); |
| num_flatten_imports_limit_exceeded_ = stats->GetVariable(kLimitExceeded); |
| num_flatten_imports_minify_failed_ = stats->GetVariable(kMinifyFailed); |
| num_flatten_imports_recursion_ = stats->GetVariable(kRecursion); |
| num_flatten_imports_complex_queries_ = stats->GetVariable(kComplexQueries); |
| } |
| |
| CssFilter::~CssFilter() {} |
| |
| void CssFilter::InitStats(Statistics* statistics) { |
| statistics->AddVariable(CssFilter::kBlocksRewritten); |
| statistics->AddVariable(CssFilter::kParseFailures); |
| statistics->AddVariable(CssFilter::kFallbackRewrites); |
| statistics->AddVariable(CssFilter::kFallbackFailures); |
| statistics->AddVariable(CssFilter::kRewritesDropped); |
| statistics->AddUpDownCounter(CssFilter::kTotalBytesSaved); |
| statistics->AddVariable(CssFilter::kTotalOriginalBytes); |
| statistics->AddVariable(CssFilter::kUses); |
| statistics->AddVariable(CssFilter::kCharsetMismatch); |
| statistics->AddVariable(CssFilter::kInvalidUrl); |
| statistics->AddVariable(CssFilter::kLimitExceeded); |
| statistics->AddVariable(CssFilter::kMinifyFailed); |
| statistics->AddVariable(CssFilter::kRecursion); |
| statistics->AddVariable(CssFilter::kComplexQueries); |
| } |
| |
| namespace { |
| |
| // Merges arrays a & b and returns the result, allocated with new[]. Checks |
| // that the arrays were non-overlapping by verifying the size of the output |
| // array. |
| template<typename T> |
| T* MergeArrays(const T* a, int a_size, const T* b, int b_size, int* out_size) { |
| *out_size = a_size + b_size; |
| T* out = new T[*out_size]; |
| T* out_end = std::merge(a, a + a_size, b, b + b_size, out); |
| CHECK_EQ(*out_size, out_end - out); |
| return out; |
| } |
| |
| } // namespace |
| |
| void CssFilter::Initialize() { |
| InitializeAtExitManager(); |
| |
| CHECK(merged_filters_ == NULL); |
| #ifndef NDEBUG |
| for (int i = 1; i < kRelatedFiltersSize; ++i) { |
| CHECK_LT(kRelatedFilters[i - 1], kRelatedFilters[i]) |
| << "kRelatedFilters not in enum-value order"; |
| } |
| #endif |
| |
| merged_filters_ = MergeArrays(ImageRewriteFilter::kRelatedFilters, |
| ImageRewriteFilter::kRelatedFiltersSize, |
| kRelatedFilters, kRelatedFiltersSize, |
| &merged_filters_size_); |
| |
| CHECK(related_options_ == NULL); |
| related_options_ = new StringPieceVector; |
| ImageRewriteFilter::AddRelatedOptions(related_options_); |
| CssFilter::AddRelatedOptions(related_options_); |
| std::sort(related_options_->begin(), related_options_->end()); |
| } |
| |
| void CssFilter::Terminate() { |
| // Note: This is not thread-safe, but I don't believe we need it to be. |
| if (at_exit_manager != NULL) { |
| delete at_exit_manager; |
| at_exit_manager = NULL; |
| } |
| |
| CHECK(merged_filters_ != NULL); |
| delete [] merged_filters_; |
| merged_filters_ = NULL; |
| CHECK(related_options_ != NULL); |
| delete related_options_; |
| related_options_ = NULL; |
| } |
| |
| void CssFilter::AddRelatedOptions(StringPieceVector* target) { |
| for (int i = 0, n = arraysize(kRelatedOptions); i < n; ++i) { |
| target->push_back(kRelatedOptions[i]); |
| } |
| } |
| |
| void CssFilter::InitializeAtExitManager() { |
| // Note: This is not thread-safe, but I don't believe we need it to be. |
| if (at_exit_manager == NULL) { |
| at_exit_manager = new base::AtExitManager; |
| } |
| } |
| |
| void CssFilter::StartDocumentImpl() { |
| in_style_element_ = false; |
| meta_tag_charset_.clear(); |
| } |
| |
| void CssFilter::StartElementImpl(HtmlElement* element) { |
| // HtmlParse should not pass us elements inside a style element. |
| CHECK(!in_style_element_); |
| if (element->keyword() == HtmlName::kStyle) { |
| in_style_element_ = true; |
| style_element_ = element; |
| } else if (driver()->can_rewrite_resources()) { |
| bool do_rewrite = false; |
| bool check_for_url = false; |
| if (driver()->options()->Enabled(RewriteOptions::kRewriteStyleAttributes)) { |
| do_rewrite = true; |
| } else if (driver()->options()->Enabled( |
| RewriteOptions::kRewriteStyleAttributesWithUrl)) { |
| check_for_url = true; |
| } |
| |
| // Rewrite style attribute, if any, and iff enabled. |
| if (do_rewrite || check_for_url) { |
| // Per http://www.w3.org/TR/CSS21/syndata.html#uri s4.3.4 URLs and URIs: |
| // "The format of a URI value is 'url(' followed by ..." |
| HtmlElement::Attribute* element_style = element->FindAttribute( |
| HtmlName::kStyle); |
| if (element_style != NULL) { |
| bool has_url = |
| CssTagScanner::HasUrl(element_style->DecodedValueOrNull()); |
| if (!check_for_url || has_url) { |
| StartAttributeRewrite( |
| element, element_style, |
| has_url ? kAttributeWithUrls : kAttributeWithoutUrls); |
| } |
| } |
| } |
| } |
| // We deal with <link> elements in EndElement. |
| } |
| |
| void CssFilter::Characters(HtmlCharactersNode* characters_node) { |
| if (in_style_element_ && driver()->can_rewrite_resources()) { |
| // Note: HtmlParse should guarantee that we only get one CharactersNode |
| // per <style> block even if it is split by a flush. However, this code |
| // will still mostly work if we somehow got multiple CharacterNodes. |
| StartInlineRewrite(characters_node); |
| } |
| } |
| |
| void CssFilter::EndElementImpl(HtmlElement* element) { |
| // Rewrite an inline style. |
| if (in_style_element_) { |
| CHECK(style_element_ == element); // HtmlParse should not pass unmatching. |
| in_style_element_ = false; |
| } |
| if (driver()->IsRewritable(element)) { |
| resource_tag_scanner::UrlCategoryVector attributes; |
| resource_tag_scanner::ScanElement( |
| element, driver()->options(), &attributes); |
| resource_tag_scanner::UrlCategoryVector::iterator uc; |
| for (uc = attributes.begin(); uc != attributes.end(); uc++) { |
| if (uc->category == semantic_type::kStylesheet) { |
| StartExternalRewrite(element, uc->url); |
| } |
| } |
| } |
| if (meta_tag_charset_.empty() && element->keyword() == HtmlName::kMeta) { |
| // Note any meta tag charset specifier. |
| GoogleString content, mime_type, charset; |
| if (ExtractMetaTagDetails(*element, NULL, &content, &mime_type, &charset)) { |
| meta_tag_charset_ = charset; |
| } |
| } |
| } |
| |
| void CssFilter::StartInlineRewrite(HtmlCharactersNode* char_node) { |
| ResourcePtr input_resource(MakeInlineResource(char_node->contents())); |
| ResourceSlotPtr slot(driver()->GetInlineSlot(input_resource, char_node)); |
| |
| CssFilter::Context* rewriter = StartRewriting(slot); |
| if (rewriter == NULL) { |
| return; |
| } |
| HtmlElement* element = char_node->parent(); |
| rewriter->SetupInlineRewrite(element, char_node); |
| |
| // Get the applicable media and charset. As style elements can't have a |
| // charset attribute pass NULL to GetApplicableCharset instead of 'element'. |
| // If the resulting charset for the style element doesn't agree with that of |
| // the source page, we can't flatten (though that should be impossible since |
| // we only look at meta elements and headers in this case). |
| CssHierarchy* hierarchy = rewriter->mutable_hierarchy(); |
| GetApplicableMedia(element, hierarchy->mutable_media()); |
| GoogleString failure_reason; |
| hierarchy->set_flattening_succeeded( |
| GetApplicableCharset(NULL, hierarchy->mutable_charset(), |
| &failure_reason)); |
| if (!hierarchy->flattening_succeeded()) { |
| num_flatten_imports_charset_mismatch_->Add(1); |
| hierarchy->AddFlatteningFailureReason(failure_reason); |
| } |
| } |
| |
| void CssFilter::StartAttributeRewrite(HtmlElement* element, |
| HtmlElement::Attribute* style, |
| InlineCssKind inline_css_kind) { |
| ResourcePtr input_resource(MakeInlineResource(style->DecodedValueOrNull())); |
| ResourceSlotPtr slot( |
| driver()->GetInlineAttributeSlot(input_resource, element, style)); |
| |
| CssFilter::Context* rewriter = StartRewriting(slot); |
| if (rewriter == NULL) { |
| return; |
| } |
| rewriter->SetupAttributeRewrite(element, style, inline_css_kind); |
| |
| // @import is not allowed (nor handled) in attribute CSS, which must be |
| // declarations only, so disable flattening from the get-go. Since this |
| // is not a failure to flatten as such, don't update the statistics. |
| // Not setting the failure reason suppresses +debug from emitting it. |
| rewriter->mutable_hierarchy()->set_flattening_succeeded(false); |
| } |
| |
| void CssFilter::StartExternalRewrite(HtmlElement* link, |
| HtmlElement::Attribute* src) { |
| if (!driver()->can_rewrite_resources()) { |
| return; |
| } |
| // Create the input resource for the slot. |
| ResourcePtr input_resource(CreateInputResourceOrInsertDebugComment( |
| src->DecodedValueOrNull(), link)); |
| if (input_resource.get() == NULL) { |
| return; |
| } |
| ResourceSlotPtr slot(driver()->GetSlot(input_resource, link, src)); |
| CssFilter::Context* rewriter = StartRewriting(slot); |
| if (rewriter == NULL) { |
| return; |
| } |
| GoogleUrl input_resource_gurl(input_resource->url()); |
| // TODO(sligocki): I don't think css_trim_gurl_ should be set to |
| // decoded_base_url(). But I also think that the values passed in here |
| // will always be overwritten later. This should be cleaned up. |
| rewriter->SetupExternalRewrite(link, input_resource_gurl, decoded_base_url()); |
| |
| // Get the applicable media and charset. If the charset on the link doesn't |
| // agree with that of the source page, we can't flatten. |
| CssHierarchy* hierarchy = rewriter->mutable_hierarchy(); |
| GetApplicableMedia(link, hierarchy->mutable_media()); |
| GoogleString failure_reason; |
| hierarchy->set_flattening_succeeded( |
| GetApplicableCharset(link, hierarchy->mutable_charset(), |
| &failure_reason)); |
| if (!hierarchy->flattening_succeeded()) { |
| num_flatten_imports_charset_mismatch_->Add(1); |
| hierarchy->AddFlatteningFailureReason(failure_reason); |
| } |
| } |
| |
| ResourcePtr CssFilter::MakeInlineResource(StringPiece content) { |
| GoogleString data_url; |
| // TODO(morlovich): This does a lot of useless conversions and |
| // copying. Get rid of them. |
| DataUrl(kContentTypeCss, PLAIN, content, &data_url); |
| return DataUrlInputResource::Make(data_url, driver()); |
| } |
| |
| CssFilter::Context* CssFilter::StartRewriting(const ResourceSlotPtr& slot) { |
| // Create the context add it to the slot, then kick everything off. |
| DCHECK(driver()->can_rewrite_resources()); |
| CssFilter::Context* rewriter = MakeContext(driver(), NULL); |
| rewriter->AddSlot(slot); |
| if (driver()->options()->css_preserve_urls()) { |
| slot->set_preserve_urls(true); |
| } |
| if (!driver()->InitiateRewrite(rewriter)) { |
| rewriter = NULL; |
| } |
| return rewriter; |
| } |
| |
| bool CssFilter::GetApplicableCharset(const HtmlElement* element, |
| GoogleString* charset, |
| GoogleString* failure_reason) const { |
| // HTTP1.1 says the default charset is ISO-8859-1 but as the W3C says (in |
| // http://www.w3.org/International/O-HTTP-charset.en.php) not many browsers |
| // actually do this so a default of "" might be better. Starting from that |
| // base, if the headers specify a charset that is used, otherwise if a meta |
| // tag specifies a charset that is used. |
| StringPiece our_charset("iso-8859-1"); |
| const char* our_charset_source = "the default"; |
| GoogleString headers_charset; |
| const ResponseHeaders* headers = driver()->response_headers(); |
| if (headers != NULL) { |
| headers_charset = headers->DetermineCharset(); |
| if (!headers_charset.empty()) { |
| our_charset = headers_charset; |
| our_charset_source = "from headers"; |
| } |
| } |
| if (headers_charset.empty() && !meta_tag_charset_.empty()) { |
| our_charset = meta_tag_charset_; |
| our_charset_source = "from a meta tag"; |
| } |
| if (element != NULL) { |
| const HtmlElement::Attribute* charset_attribute = |
| element->FindAttribute(HtmlName::kCharset); |
| if (charset_attribute != NULL) { |
| const char* elements_charset = charset_attribute->DecodedValueOrNull(); |
| if (our_charset != elements_charset) { |
| *failure_reason = StrCat( |
| "The charset of the HTML (", our_charset, ", ", our_charset_source, |
| ") is different from the charset attribute on the preceding " |
| "element (", |
| (elements_charset == NULL ? "not set" : elements_charset), ")"); |
| return false; // early return! |
| } |
| } |
| } |
| our_charset.CopyToString(charset); |
| return true; |
| } |
| |
| bool CssFilter::GetApplicableMedia(const HtmlElement* element, |
| StringVector* media) const { |
| bool result = false; |
| if (element != NULL) { |
| const HtmlElement::Attribute* media_attribute = |
| element->FindAttribute(HtmlName::kMedia); |
| if (media_attribute != NULL) { |
| css_util::VectorizeMediaAttribute(media_attribute->DecodedValueOrNull(), |
| media); |
| result = true; |
| } |
| } |
| return result; |
| } |
| |
| CssFilter::Context* CssFilter::MakeContext(RewriteDriver* driver, |
| RewriteContext* parent) { |
| ResourceContext* resource_context = new ResourceContext; |
| if (parent != NULL && parent->resource_context() != NULL) { |
| resource_context->CopyFrom(*(parent->resource_context())); |
| } else { |
| EncodeUserAgentIntoResourceContext(resource_context); |
| } |
| return new Context(this, driver, parent, cache_extender_, |
| image_rewrite_filter_, image_combiner_, resource_context); |
| } |
| |
| RewriteContext* CssFilter::MakeRewriteContext() { |
| return MakeContext(driver(), NULL); |
| } |
| |
| const UrlSegmentEncoder* CssFilter::encoder() const { |
| return &encoder_; |
| } |
| |
| void CssFilter::EncodeUserAgentIntoResourceContext( |
| ResourceContext* context) const { |
| // Use the same encoding as the image rewrite filter. |
| image_rewrite_filter_->EncodeUserAgentIntoResourceContext(context); |
| } |
| |
| const UrlSegmentEncoder* CssFilter::Context::encoder() const { |
| return filter_->encoder(); |
| } |
| |
| RewriteContext* CssFilter::MakeNestedRewriteContext( |
| RewriteContext* parent, const ResourceSlotPtr& slot) { |
| RewriteContext* context = MakeContext(NULL, parent); |
| context->AddSlot(slot); |
| return context; |
| } |
| |
| RewriteContext* CssFilter::MakeNestedFlatteningContextInNewSlot( |
| const ResourcePtr& resource, const GoogleString& location, |
| CssFilter::Context* rewriter, RewriteContext* parent, |
| CssHierarchy* hierarchy) { |
| // Slot represents the @import URL inside another CSS file. But rendering is |
| // complicated, so we use a NullResourceSlot that has an empty Render method. |
| ResourceSlotPtr slot(new NullResourceSlot(resource, location)); |
| RewriteContext* context = new CssFlattenImportsContext(parent, this, |
| rewriter, hierarchy); |
| context->AddSlot(slot); |
| return context; |
| } |
| |
| } // namespace net_instaweb |