1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ios/web/webui/crw_web_ui_page_builder.h"
11 #include "base/ios/weak_nsobject.h"
12 #include "base/logging.h"
13 #include "base/mac/bundle_locations.h"
14 #include "base/mac/scoped_nsobject.h"
15 #include "base/strings/sys_string_conversions.h"
18 // Prefix for script tags. Used to locate JavaScript subresources.
19 NSString* const kJSTagPrefix = @"<script src=\"";
20 // Suffix for script tags. Used to locate JavaScript subresources.
21 NSString* const kJSTagSuffix = @"\"></script>";
22 // Prefix for stylesheet tags. Used to locate CSS subresources.
23 NSString* const kCSSTagPrefix = @"<link rel=\"stylesheet\" href=\"";
24 // Suffix for stylesheet tags. Used to locate CSS subresources.
25 NSString* const kCSSTagSuffix = @"\">";
26 // Template for creating inlined JavaScript tags.
27 NSString* const kWebUIScriptTextTemplate = @"<script>%@</script>";
28 // Template for creating inlined CSS tags.
29 NSString* const kWebUIStyleTextTemplate = @"<style>%@</style>";
30 // URL placeholder for WebUI messaging JavaScript.
31 NSString* const kWebUIJSURL = @"chrome://resources/js/ios/web_ui.js";
34 @interface CRWWebUIPageBuilder ()
36 // Builds WebUI page for URL with HTML as default resource.
37 - (void)buildWebUIPageForHTML:(NSString*)HTML
38 webUIURL:(const GURL&)URL
39 completionHandler:(web::WebUIPageCompletion)completionHandler;
41 // Loads |resourceURL| by invoking _delegate.
42 - (void)fetchResourceWithURL:(const GURL&)resourceURL
43 completionHandler:(web::WebUIDelegateCompletion)handler;
45 // Loads |resourceURLs| by invoking _delegate. Members of resourceURLs must be
46 // valid subresource URLs. |completionHandler| is called upon load of each
48 - (void)fetchSubresourcesWithURLs:(const std::vector<GURL>&)resourceURLs
49 completionHandler:(web::WebUIDelegateCompletion)handler;
51 // Looks for substrings surrounded by |prefix| and |suffix| in resource and
52 // returns them as an NSSet.
53 // For example, if prefix is "<a href='", suffix is "'>", and |resource|
54 // includes substrings "<a href='http://www.apple.com'>" and
55 // "<a href='chrome.html'>", return value will contain strings
56 // "http://www.apple.com" and "chrome.html".
57 - (NSSet*)URLStringsFromResource:(NSString*)resource
58 prefix:(NSString*)prefix
59 suffix:(NSString*)suffix;
61 // Returns URL strings for subresources found in |HTML|. URLs are found by
62 // searching for tags of the format <link rel="stylesheet" href="URLString">
63 // and <script src="URLString">.
64 - (NSSet*)URLStringsFromHTML:(NSString*)HTML;
66 // Returns URL strings for subresources found in |CSS|. URLs are found by
67 // searching for statements of the format @import(URLString).
68 - (NSSet*)URLStringsFromCSS:(NSString*)CSS;
70 // YES if subresourceURL is a valid subresource URL. Valid subresource URLs
71 // include files with extension ".js" and ".css".
72 - (BOOL)isValidSubresourceURL:(const GURL&)subresourceURL;
74 // YES if subresourceURL is for a CSS resource.
75 - (BOOL)isCSSSubresourceURL:(const GURL&)subresourceURL;
77 // Prepends "<link rel="stylesheet" href="|URL|">" to the HTML link tag with
78 // href=|sourceURL| in |HTML|.
79 - (void)addCSSTagToHTML:(NSMutableString*)HTML
80 forURL:(const GURL&)URL
81 sourceURL:(const GURL&)sourceURL;
83 // Flattens HTML with provided map of URLs to resource strings.
84 - (void)flattenHTML:(NSMutableString*)HTML
85 withSubresources:(const std::map<GURL, std::string>&)subresources;
87 // Returns JavaScript needed for bridging WebUI chrome.send() messages to
88 // the core.js defined message delivery system.
89 - (NSString*)webUIJavaScript;
93 @implementation CRWWebUIPageBuilder {
94 // Delegate for requesting resources.
95 base::WeakNSProtocol<id<CRWWebUIPageBuilderDelegate>> _delegate;
98 #pragma mark - Public Methods
100 - (instancetype)init {
105 - (instancetype)initWithDelegate:(id<CRWWebUIPageBuilderDelegate>)delegate {
106 if (self = [super init]) {
107 _delegate.reset(delegate);
112 - (void)buildWebUIPageForURL:(const GURL&)webUIURL
113 completionHandler:(web::WebUIPageCompletion)completionHandler {
114 [self fetchResourceWithURL:webUIURL
115 completionHandler:^(NSString* webUIHTML, const GURL& URL) {
116 [self buildWebUIPageForHTML:webUIHTML
118 completionHandler:completionHandler];
122 #pragma mark - Private Methods
124 - (void)buildWebUIPageForHTML:(NSString*)HTML
125 webUIURL:(const GURL&)pageURL
126 completionHandler:(web::WebUIPageCompletion)completionHandler {
127 __block base::scoped_nsobject<NSMutableString> webUIHTML([HTML mutableCopy]);
128 NSSet* subresourceURLStrings = [self URLStringsFromHTML:webUIHTML];
129 __block NSUInteger pendingSubresourceCount = [subresourceURLStrings count];
130 if (!pendingSubresourceCount) {
131 completionHandler(webUIHTML);
134 __block std::map<GURL, std::string> subresources;
135 base::WeakNSObject<CRWWebUIPageBuilder> weakSelf(self);
136 // Completion handler for subresource loads.
137 __block web::WebUIDelegateCompletion subresourceHandler = nil;
138 subresourceHandler = [[^(NSString* subresource, const GURL& subresourceURL) {
139 // Import statements in CSS resources are also loaded.
140 if ([self isCSSSubresourceURL:subresourceURL]) {
141 NSSet* URLStrings = [weakSelf URLStringsFromCSS:subresource];
142 for (NSString* URLString in URLStrings) {
143 GURL URL(subresourceURL.Resolve(base::SysNSStringToUTF8(URLString)));
144 [weakSelf addCSSTagToHTML:webUIHTML
146 sourceURL:subresourceURL];
147 ++pendingSubresourceCount;
148 [weakSelf fetchResourceWithURL:URL
149 completionHandler:subresourceHandler];
152 subresources[subresourceURL] = base::SysNSStringToUTF8(subresource);
153 --pendingSubresourceCount;
154 // When subresource loading is complete, flatten the default resource
155 // and invoke the completion handler.
156 if (!pendingSubresourceCount) {
157 [weakSelf flattenHTML:webUIHTML withSubresources:subresources];
158 completionHandler(webUIHTML);
160 } copy] autorelease];
162 for (NSString* URLString in subresourceURLStrings) {
163 // chrome://resources/js/ios/web_ui.js is skipped because it is
164 // retrieved via webUIJavaScript rather than the net stack.
165 if ([URLString isEqualToString:kWebUIJSURL]) {
166 --pendingSubresourceCount;
167 if (!pendingSubresourceCount) {
168 [weakSelf flattenHTML:webUIHTML withSubresources:subresources];
169 completionHandler(webUIHTML);
173 GURL URL(pageURL.Resolve(base::SysNSStringToUTF8(URLString)));
174 // If the resolved URL is different from URLString, replace URLString in
175 // webUIHTML so that it will be located appropriately when flattening
177 if (URL.spec() != base::SysNSStringToUTF8(URLString)) {
178 [webUIHTML replaceOccurrencesOfString:URLString
179 withString:base::SysUTF8ToNSString(URL.spec())
181 range:NSMakeRange(0, [HTML length])];
183 [self fetchResourceWithURL:URL completionHandler:subresourceHandler];
187 - (void)fetchResourceWithURL:(const GURL&)resourceURL
188 completionHandler:(web::WebUIDelegateCompletion)handler {
189 [_delegate webUIPageBuilder:self
190 fetchResourceWithURL:resourceURL
191 completionHandler:handler];
194 - (void)fetchSubresourcesWithURLs:(const std::vector<GURL>&)resourceURLs
195 completionHandler:(web::WebUIDelegateCompletion)handler {
196 for (const GURL& URL : resourceURLs) {
197 DCHECK([self isValidSubresourceURL:URL]);
198 [self fetchResourceWithURL:URL completionHandler:handler];
202 - (NSSet*)URLStringsFromResource:(NSString*)resource
203 prefix:(NSString*)prefix
204 suffix:(NSString*)suffix {
208 NSMutableSet* URLStrings = [NSMutableSet set];
209 NSError* error = nil;
210 NSString* tagPattern = [NSString
211 stringWithFormat:@"%@(.*?)%@",
212 [NSRegularExpression escapedPatternForString:prefix],
213 [NSRegularExpression escapedPatternForString:suffix]];
214 NSRegularExpression* tagExpression = [NSRegularExpression
215 regularExpressionWithPattern:tagPattern
216 options:NSRegularExpressionCaseInsensitive
219 DLOG(WARNING) << "Error: " << error.description.UTF8String;
223 [tagExpression matchesInString:resource
225 range:NSMakeRange(0, [resource length])];
226 for (NSTextCheckingResult* match in matches) {
227 NSRange matchRange = [match rangeAtIndex:1];
228 DCHECK(matchRange.length);
229 NSString* URLString = [resource substringWithRange:matchRange];
230 [URLStrings addObject:URLString];
235 - (NSSet*)URLStringsFromHTML:(NSString*)HTML {
236 NSSet* JS = [self URLStringsFromResource:HTML
238 suffix:kJSTagSuffix];
239 NSSet* CSS = [self URLStringsFromResource:HTML
241 suffix:kCSSTagSuffix];
242 return [JS setByAddingObjectsFromSet:CSS];
245 - (NSSet*)URLStringsFromCSS:(NSString*)CSS {
246 NSString* prefix = @"@import url(";
247 NSString* suffix = @");";
248 return [self URLStringsFromResource:CSS prefix:prefix suffix:suffix];
251 - (BOOL)isValidSubresourceURL:(const GURL&)subresourceURL {
252 base::FilePath resourcePath(subresourceURL.ExtractFileName());
253 std::string extension = resourcePath.Extension();
254 return extension == ".css" || extension == ".js";
257 - (BOOL)isCSSSubresourceURL:(const GURL&)subresourceURL {
258 base::FilePath resourcePath(subresourceURL.ExtractFileName());
259 std::string extension = resourcePath.Extension();
260 return extension == ".css";
263 - (void)addCSSTagToHTML:(NSMutableString*)HTML
264 forURL:(const GURL&)URL
265 sourceURL:(const GURL&)sourceURL {
266 NSString* URLString = base::SysUTF8ToNSString(URL.spec());
267 NSString* sourceURLString = base::SysUTF8ToNSString(sourceURL.spec());
268 NSString* sourceTag =
269 [NSString stringWithFormat:@"%@%@%@", kCSSTagPrefix, sourceURLString,
271 NSString* extendedTag = [[NSString
272 stringWithFormat:@"%@%@%@", kCSSTagPrefix, URLString, kCSSTagSuffix]
273 stringByAppendingString:sourceTag];
274 [HTML replaceOccurrencesOfString:sourceTag
275 withString:extendedTag
277 range:NSMakeRange(0, [HTML length])];
280 - (void)flattenHTML:(NSMutableString*)HTML
281 withSubresources:(const std::map<GURL, std::string>&)subresources {
282 // Add core.js script to resources.
283 // TODO(jyquinn): Move inclusion of this resource into WebUI implementation
284 // rather than forking each HTML file (crbug.com/487000).
285 GURL webUIJSURL("chrome://resources/js/ios/web_ui.js");
286 std::map<GURL, std::string> resources(subresources);
287 resources[webUIJSURL] = base::SysNSStringToUTF8([self webUIJavaScript]);
288 NSString* linkTemplateCSS =
289 [NSString stringWithFormat:@"%@%%@%@", kCSSTagPrefix, kCSSTagSuffix];
290 NSString* linkTemplateJS =
291 [NSString stringWithFormat:@"%@%%@%@", kJSTagPrefix, kJSTagSuffix];
292 for (auto it = resources.begin(); it != resources.end(); it++) {
293 NSString* linkTemplate = @"";
294 NSString* textTemplate = @"";
295 if ([self isCSSSubresourceURL:it->first]) {
296 linkTemplate = linkTemplateCSS;
297 textTemplate = kWebUIStyleTextTemplate;
298 } else { // JavaScript.
299 linkTemplate = linkTemplateJS;
300 textTemplate = kWebUIScriptTextTemplate;
302 NSString* resourceURLString = base::SysUTF8ToNSString(it->first.spec());
304 [NSString stringWithFormat:linkTemplate, resourceURLString];
305 NSString* resource = base::SysUTF8ToNSString(it->second);
306 NSString* textTag = [NSString stringWithFormat:textTemplate, resource];
307 [HTML replaceOccurrencesOfString:linkTag
310 range:NSMakeRange(0, [HTML length])];
314 - (NSString*)webUIJavaScript {
315 NSBundle* bundle = base::mac::FrameworkBundle();
316 NSString* path = [bundle pathForResource:@"web_ui" ofType:@"js"];
317 DCHECK(path) << "web_ui.js file not found";
318 return [NSString stringWithContentsOfFile:path
319 encoding:NSUTF8StringEncoding