Add ICU message format support
[chromium-blink-merge.git] / ios / web / webui / crw_web_ui_page_builder.mm
blob03c2c6a3f0b313325417b3862ae13228f8674189
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"
7 #include <map>
8 #include <string>
9 #include <vector>
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"
17 namespace {
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";
32 }  // namespace
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
47 // resource.
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;
91 @end
93 @implementation CRWWebUIPageBuilder {
94   // Delegate for requesting resources.
95   base::WeakNSProtocol<id<CRWWebUIPageBuilderDelegate>> _delegate;
98 #pragma mark - Public Methods
100 - (instancetype)init {
101   NOTREACHED();
102   return self;
105 - (instancetype)initWithDelegate:(id<CRWWebUIPageBuilderDelegate>)delegate {
106   if (self = [super init]) {
107     _delegate.reset(delegate);
108   }
109   return self;
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
117                                 webUIURL:URL
118                        completionHandler:completionHandler];
119            }];
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);
132     return;
133   }
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
145                            forURL:URL
146                         sourceURL:subresourceURL];
147         ++pendingSubresourceCount;
148         [weakSelf fetchResourceWithURL:URL
149                      completionHandler:subresourceHandler];
150       }
151     }
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);
159     }
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);
170       }
171       continue;
172     }
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
176     // occurs.
177     if (URL.spec() != base::SysNSStringToUTF8(URLString)) {
178       [webUIHTML replaceOccurrencesOfString:URLString
179                                  withString:base::SysUTF8ToNSString(URL.spec())
180                                     options:0
181                                       range:NSMakeRange(0, [HTML length])];
182     }
183     [self fetchResourceWithURL:URL completionHandler:subresourceHandler];
184   }
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];
199   }
202 - (NSSet*)URLStringsFromResource:(NSString*)resource
203                           prefix:(NSString*)prefix
204                           suffix:(NSString*)suffix {
205   DCHECK(resource);
206   DCHECK(prefix);
207   DCHECK(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
217                              error:&error];
218   if (error) {
219     DLOG(WARNING) << "Error: " << error.description.UTF8String;
220     return URLStrings;
221   }
222   NSArray* matches =
223       [tagExpression matchesInString:resource
224                              options:0
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];
231   }
232   return URLStrings;
235 - (NSSet*)URLStringsFromHTML:(NSString*)HTML {
236   NSSet* JS = [self URLStringsFromResource:HTML
237                                     prefix:kJSTagPrefix
238                                     suffix:kJSTagSuffix];
239   NSSet* CSS = [self URLStringsFromResource:HTML
240                                      prefix:kCSSTagPrefix
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,
270                                  kCSSTagSuffix];
271   NSString* extendedTag = [[NSString
272       stringWithFormat:@"%@%@%@", kCSSTagPrefix, URLString, kCSSTagSuffix]
273       stringByAppendingString:sourceTag];
274   [HTML replaceOccurrencesOfString:sourceTag
275                         withString:extendedTag
276                            options:0
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;
301     }
302     NSString* resourceURLString = base::SysUTF8ToNSString(it->first.spec());
303     NSString* linkTag =
304         [NSString stringWithFormat:linkTemplate, resourceURLString];
305     NSString* resource = base::SysUTF8ToNSString(it->second);
306     NSString* textTag = [NSString stringWithFormat:textTemplate, resource];
307     [HTML replaceOccurrencesOfString:linkTag
308                           withString:textTag
309                              options:0
310                                range:NSMakeRange(0, [HTML length])];
311   }
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
320                                       error:nil];
323 @end