Fix breakages in https://codereview.chromium.org/1155713003/
[chromium-blink-merge.git] / ios / web / net / clients / crw_js_injection_network_client.mm
blob02f0900ed216e607c4ccf8c065aadda926310261
1 // Copyright 2014 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 #import "ios/web/net/clients/crw_js_injection_network_client.h"
7 #include "base/logging.h"
8 #include "base/mac/objc_property_releaser.h"
9 #include "base/mac/scoped_nsobject.h"
10 #include "base/metrics/histogram.h"
11 #import "ios/net/crn_http_url_response.h"
12 #import "ios/third_party/blink/src/html_tokenizer.h"
14 // CRWJSInjectionNetworkClient injects an external script tag reference for
15 // crweb.js into HTML and XHTML documents. To do this correctly, three data
16 // points are needed: where to inject the script tag, what encoding the content
17 // is in (ASCII compatible, UTF32, UTF16 and big or little endian) and the byte
18 // length of the injection tag itself.
20 // The content encoding is handled first. As data is received,
21 // CRWJSInjectionNetworkClient will look at the beginning few bytes of the
22 // content for either a byte-order mark or an XML declaration. If present, they
23 // will be matched to a known set of patterns to determine the encoding. If not
24 // present, by definition, the content is in an ASCII compatible encoding (at
25 // least with regard to the markup tags themselves, which is all
26 // CRWJSInjectionNetworkClient cares about).
28 // Next, CRWJSInjectionNetworkClient will look for the byte offset at which to
29 // inject. For HTML and XHTML documents to work correctly, the script tag must
30 // be injected after the "<html>" tag (right after the '>' character). To
31 // determine the byte offset of this character, blink's HTMLTokenizer is used.
32 // Specifically, as data is received from the network,
33 // CRWJSInjectionNetworkClient will buffer the data until a sufficient amount of
34 // data is collected to scan for the "<html>" tag. This scan is repeated, as
35 // data is received, until one of three things happens: 1) an "<html>" tag is
36 // found, 2) some tag OTHER than an "<html>" tag is found, or 3) no tag at all
37 // is found within the first 1024 bytes of the content.
39 // For case 3) above, nothing is injected and the NSURLHTTPResponse as well as
40 // all associated data are immediately dispatched in unaltered form to their
41 // intended recipient (WebKit). All subsequently received data is simply passed
42 // along to its destination unaltered.
44 // For cases 1) & 2) above, the "Content-Length" header of the NSURLHTTPResponse
45 // object will be updated as appropriate (since NSURLHTTPResponse is unmutable,
46 // this means a copy is made) and then dispatched. Immediately after this, the
47 // data is dispatched in three chunks:
48 //  - Everything before the injection byte offset.
49 //  - The appropriately content encoded <script> tag string.
50 //  - All remaining data.
52 // All subsequently received data is simply passed along to its destination
53 // unaltered.
56 namespace {
58 // When looking for the meta-charset tag, blink/webkit
59 // limits itself to the first 1024 bytes.
60 const size_t kLimitOfBytesToCheckForHeadTag = 1024;
61 const size_t kMinimumBytesNeededForHTMLTag = 7;
62 const size_t kMinimumBytesNeededForBOM = 4;
63 const size_t kMinimumBytesNeededForXMLDecl = 8;
65 NSString* const kContentLength = @"Content-Length";
67 NSString* const kJSContentTemplate =
68     @"<script src=\"%@_crweb.js\" charset=\"utf-8\"></script>";
70 // Compares a value pointed to by |bytes| to a single value |byte|. Returns
71 // true if they are equal, false otherwise. This is a base case for the
72 // variadic template version of the method of the same name below.
73 template <typename T1, typename T2>
74 bool BytesEqual(const T1* bytes, T2 byte) {
75   DCHECK(bytes);
76   return (*bytes == byte);
79 // Compares an array of values |bytes| to a value |byte| and a variable number
80 // of other values specified by |args|. Returns true if the values pointed to
81 // by |bytes| are equal to values |byte| through |args| in positional order,
82 // false otherwise. This means that if true is returned, values bytes[0...N] and
83 // byte...args are all equal.
84 template <typename T1, typename T2, typename... Arguments>
85 bool BytesEqual(const T1* bytes, T2 byte, Arguments... args) {
86   DCHECK(bytes);
87   if (*bytes == byte)
88     return BytesEqual(++bytes, args...);
90   return false;
93 // Gets the value corresponding to the key "Content-Length" in the passed
94 // |allHeaders| dictionary. If no such key is present in |allHeaders| returns
95 // -1.
96 long long GetContentLengthFromAllHeaders(NSDictionary* all_headers) {
97   NSObject* content_length_object = [all_headers objectForKey:kContentLength];
99   // This handles both the case when the object is an NSNumber and the case
100   // when the object is an NSString.
101   if ([content_length_object respondsToSelector:@selector(longLongValue)])
102     return [static_cast<id>(content_length_object) longLongValue];
104   return -1;
107 // Returns an CRNHTTPURLResponse instance with the "Content-Length"
108 // increased to include the passed |additionContentSize|. If
109 // |additionContentSize| is zero, the passed response is returned.
110 CRNHTTPURLResponse* ResponseWithUpdatedContentSize(
111     CRNHTTPURLResponse* response,
112     NSUInteger addition_content_size) {
113   if (!response)
114     return nil;
116   if (!addition_content_size)
117     return response;
119   if (![response isKindOfClass:[CRNHTTPURLResponse class]]) {
120     NOTREACHED();
121     return response;
122   }
124   // NSURLResponse uses a long long return type for expectedContentLength.
125   NSDictionary* all_headers = [response allHeaderFields];
126   long long content_length = GetContentLengthFromAllHeaders(all_headers);
127   if (content_length < 0) {
128     return response;
129   }
131   // Create a new content length value.
132   content_length += addition_content_size;
133   NSString* content_length_value =
134       [[NSNumber numberWithLongLong:content_length] stringValue];
136   base::scoped_nsobject<NSMutableDictionary> all_headers_mutable;
137   all_headers_mutable.reset([all_headers mutableCopy]);
138   [all_headers_mutable setObject:content_length_value forKey:kContentLength];
140   CRNHTTPURLResponse* update_response =
141       [[CRNHTTPURLResponse alloc] initWithURL:[response URL]
142                                    statusCode:[response statusCode]
143                                   HTTPVersion:[response cr_HTTPVersion]
144                                  headerFields:all_headers_mutable];
146   return [update_response autorelease];
148 }  // namespace
150 @interface CRWJSInjectionNetworkClient () {
151   // The CRNHTTPURLResponse that is held until it is determined if the content
152   // will have JavaScript injected.
153   base::scoped_nsobject<CRNHTTPURLResponse> _pendingResponse;
155   // An array of data that is buffered until a determination is made about
156   // injecting JavaScript.
157   base::scoped_nsobject<NSMutableArray> _pendingData;
159   // The content that will be injected in NSData form.
160   base::scoped_nsobject<NSData> _jsInjectionContent;
162   BOOL _completedByteOrderMarkCheck;
163   BOOL _completedXMLDeclarationCheck;
164   BOOL _completedCheckForWhereToInject;
166   NSStringEncoding _contentEncoding;
167   NSUInteger _headerLength;
168   NSUInteger _pendingDataLength;
170   NSUInteger _byteOffsetAtWhichToInject;
171   BOOL _proceedWithInjection;
174 // Returns YES if all checks (BOM, XML declaration, injection location) are
175 // complete.
176 - (BOOL)completedAllChecks;
178 // Records a UMA histogram indicating the injection result
179 // (see enum InjectionResult).
180 - (void)recordHistogramResult;
182 // Returns an NSData containing the correctly encoded script tag referencing
183 // the appropriate crweb.js for this web view.
184 - (NSData*)jsInjectionContent;
186 // If injection is appropriate for this content, dispatches buffered data in
187 // the form:
188 //  1) [everything PRIOR to injection byte offset]
189 //  2) [self jsInjectionContent]
190 //  3) [everything AFTER the injection byte offset]
191 - (void)sendInjectedResponseIfNeeded;
193 // Dispatches the buffered and appropriately "Content-Length"-header-updated
194 // NSURLHTTPResponse to the next level network client (almost certainly
195 // WebKit/CFNetwork itself).
196 - (void)sendPendingResponse;
198 // Calls [self sendInjectedResponseIfNeeded] and then sends any remaining
199 // buffered data to the next level network client.
200 - (void)sendPendingData;
202 // Coalesces data contained in self.pendingData until a single NSData object of
203 // at least length |lengthNeeded| has been created and inserted into
204 // self.pendingData as the first element.
205 - (void)coalesceDataIfNeeded:(NSUInteger)lengthNeeded;
207 // Looks for a byte order mark in the content to indicate character encoding.
208 // Sets _completedByteOrderMarkCheck to YES once complete and updates
209 // _contentEncoding if an encoding has been determined.
210 - (void)checkForByteOrderMark;
212 // Looks for an XML declaration in the content to indicate character encoding.
213 // Sets _completedXMLDeclarationCheck to YES once complete and updates
214 // _contentEncoding if an encoding has been determined.
215 - (void)checkForXMLDeclaration;
217 // Checks whether the hard byte limit specified by
218 // kLimitOfBytesToCheckForHeadTag has been hit or exceeded during the check
219 // for an appropriate injection location. If the limit is hit or exceeded,
220 // _completedCheckForWhereToInject will be set to YES and
221 // -[CRWJSInjectionNetworkClient checkForWhereToInject will stop.
222 - (void)checkIfByteLimitPassed:(const WebCore::CharacterProvider&)provider;
224 // Checks for an appropriate byte offset at which to inject the external
225 // script tag. Sets _completedCheckForWhereToInject to YES once complete and
226 // updates _byteOffsetAtWhichToInject to the location at which to inject. Since
227 // this may be set to zero, _proceedWithInjection is set to YES to indicate that
228 // a location at which to inject has been found. Uses blink's HTMLTokenizer.
229 - (void)checkForWhereToInject;
230 @end
232 @implementation CRWJSInjectionNetworkClient
234 + (BOOL)canHandleResponse:(NSURLResponse*)response {
235   NSString* scheme = [[response URL] scheme];
236   if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"])
237     return NO;
239   NSString* mimeType = [response MIMEType];
241   // TODO(jyquinn): WebKit has a DOM for any XML document. Should we inject
242   // JavaScript into them and not limit to html and xhtml?
243   if ([mimeType isEqualToString:@"text/html"])
244     return YES;
246   if ([mimeType isEqualToString:@"application/xhtml+xml"])
247     return YES;
249   return NO;
252 #pragma mark CRNNetworkClientProtocol implementation
254 - (void)didFailWithNSErrorCode:(NSInteger)nsErrorCode
255                   netErrorCode:(int)netErrorCode {
256   _proceedWithInjection = NO;
257   [self sendPendingResponse];
258   [self sendPendingData];
259   [super didFailWithNSErrorCode:nsErrorCode netErrorCode:netErrorCode];
262 - (void)didLoadData:(NSData*)data {
263   if ([self completedAllChecks]) {
264     [super didLoadData:data];
265     return;
266   }
268   if (!_pendingData)
269     _pendingData.reset([[NSMutableArray alloc] init]);
271   [_pendingData addObject:data];
272   _pendingDataLength += [data length];
274   [self checkForByteOrderMark];
275   [self checkForXMLDeclaration];
276   [self checkForWhereToInject];
278   if ([self completedAllChecks]) {
279     if (!_contentEncoding)
280       _contentEncoding = NSASCIIStringEncoding;
281     [self sendPendingResponse];
282     [self sendPendingData];
283   }
286 - (void)didReceiveResponse:(NSURLResponse*)response {
287   DCHECK([response isKindOfClass:[CRNHTTPURLResponse class]]);
288   DCHECK([response expectedContentLength] ==
289          GetContentLengthFromAllHeaders(
290              [static_cast<NSHTTPURLResponse*>(response) allHeaderFields]));
291   DCHECK([CRWJSInjectionNetworkClient canHandleResponse:response]);
293   // If response does not come with Content-Length header field (i.e. the
294   // Transfer-Encoding header field has value 'chunked'), response does not have
295   // to be updated.
296   if ([response expectedContentLength] == -1) {
297     [super didReceiveResponse:response];
298   } else {
299   // Client calls [super didReceiveResponse:] in sendPendingResponse.
300     _pendingResponse.reset([static_cast<CRNHTTPURLResponse*>(response) retain]);
301   }
304 - (void)didFinishLoading {
305   [self recordHistogramResult];
306   [self sendPendingResponse];
307   [self sendPendingData];
308   [super didFinishLoading];
311 #pragma mark Internal methods
313 - (BOOL)completedAllChecks {
314   if (!_completedByteOrderMarkCheck)
315     return NO;
317   if (!_completedXMLDeclarationCheck)
318     return NO;
320   if (!_completedCheckForWhereToInject)
321     return NO;
323   return YES;
326 - (void)recordHistogramResult {
327   web::InjectionResult result = web::InjectionResult::INJECTION_RESULT_COUNT;
328   if (_proceedWithInjection)
329     result = web::InjectionResult::SUCCESS_INJECTED;
330   else if (_pendingDataLength < kMinimumBytesNeededForHTMLTag)
331     result = web::InjectionResult::FAIL_INSUFFICIENT_CONTENT_LENGTH;
332   else
333     result = web::InjectionResult::FAIL_FIND_INJECTION_LOCATION;
335   if (result < web::InjectionResult::INJECTION_RESULT_COUNT) {
336     UMA_HISTOGRAM_ENUMERATION(
337         "NetworkLayerJSInjection.Result", static_cast<int>(result),
338         static_cast<int>(web::InjectionResult::INJECTION_RESULT_COUNT));
339   } else {
340     NOTREACHED();
341   }
344 - (NSData*)jsInjectionContent {
345   DCHECK(_proceedWithInjection);
346   if (_jsInjectionContent)
347     return _jsInjectionContent;
349   NSString* jsContentString = [NSString
350       stringWithFormat:kJSContentTemplate, [[NSUUID UUID] UUIDString]];
351   _jsInjectionContent.reset(
352       [[jsContentString dataUsingEncoding:_contentEncoding] retain]);
354   return _jsInjectionContent;
357 - (void)sendInjectedResponseIfNeeded {
358   if (!_proceedWithInjection)
359     return;
361   NSData* firstData = [_pendingData firstObject];
362   NSUInteger dataLength = [firstData length];
363   if (!dataLength)
364     return;
366   const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
368   // Construct one data in which to send the content + injected script tag.
369   base::scoped_nsobject<NSMutableData> combined([[NSMutableData alloc] init]);
370   if (_byteOffsetAtWhichToInject)
371     [combined appendBytes:static_cast<const void*>(bytes)
372                    length:_byteOffsetAtWhichToInject];
374   // Send back the JavaScript content to inject.
375   [combined appendData:[self jsInjectionContent]];
376   [combined
377       appendBytes:static_cast<const void*>(bytes + _byteOffsetAtWhichToInject)
378            length:(dataLength - _byteOffsetAtWhichToInject)];
380   [super didLoadData:combined];
382   // The first data, into which the JS was injected, is no longer needed.
383   [_pendingData.get() removeObjectAtIndex:0];
384   _jsInjectionContent.reset();
387 - (void)sendPendingResponse {
388   if (!_pendingResponse)
389     return;
391   if (_proceedWithInjection) {
392     NSUInteger additionalLength = [[self jsInjectionContent] length];
393     CRNHTTPURLResponse* responseToSend =
394         ResponseWithUpdatedContentSize(_pendingResponse, additionalLength);
395     _pendingResponse.reset([responseToSend retain]);
396   }
398   [super didReceiveResponse:_pendingResponse];
399   _pendingResponse.reset();
402 - (void)sendPendingData {
403   if (![_pendingData count])
404     return;
406   [self sendInjectedResponseIfNeeded];
408   for (NSData* data in _pendingData.get())
409     [super didLoadData:data];
411   _pendingData.reset();
414 - (void)coalesceDataIfNeeded:(NSUInteger)lengthNeeded {
415   NSData* firstData = [_pendingData firstObject];
417   // Obviously if the first data object has enough data,
418   // nothing needs to be done.
419   if ([firstData length] >= lengthNeeded)
420     return;
422   // Make sure we have something that can be coalesced.
423   if ([_pendingData count] < 2)
424     return;
426   // Start with a mutable copy of the first data item.
427   base::scoped_nsobject<NSMutableData> coalescedData([firstData mutableCopy]);
429   // Replace the first item in the array.
430   [_pendingData removeObjectAtIndex:0];
432   // While not enough data has been coalesced and there are items
433   // that can be coalesced, do so.
434   while ([coalescedData length] < lengthNeeded && [_pendingData count]) {
435     [coalescedData appendData:_pendingData[0]];
436     [_pendingData removeObjectAtIndex:0];
437   }
439   // Finally, put the coalesced object at the front of the pending data array.
440   [_pendingData insertObject:coalescedData atIndex:0];
443 - (void)checkForByteOrderMark {
444   // Bail if the check has already been completed.
445   if (_completedByteOrderMarkCheck)
446     return;
448   // Bail if there is not yet enough data to check.
449   if (_pendingDataLength < kMinimumBytesNeededForBOM)
450     return;
452   // It's possible that the server returned data in small chunks, so coalesce
453   // the data into one buffer if needed.
454   [self coalesceDataIfNeeded:kMinimumBytesNeededForBOM];
456   // Get some data to investigate.
457   NSData* firstData = [_pendingData firstObject];
458   if (!firstData && [firstData length] < kMinimumBytesNeededForBOM) {
459     NOTREACHED();
460     return;
461   }
463   // Do the same check that WebKit does for the byte order mark (BOM), which
464   // must be right at the beginning of the content to be accepted.
465   // Info on byte order mark: http://en.wikipedia.org/wiki/Byte_order_mark
466   const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
467   if (BytesEqual(bytes, 0xFF, 0xFE)) {
468     bytes += 2;
470     if (!BytesEqual(bytes, 0x00, 0x00)) {
471       _contentEncoding = NSUTF16LittleEndianStringEncoding;
472       _headerLength += 2;
473     } else {
474       _contentEncoding = NSUTF32LittleEndianStringEncoding;
475       _headerLength += 4;
476     }
477   } else if (BytesEqual(bytes, 0xEF, 0xBB, 0xBF)) {
478     _contentEncoding = NSUTF8StringEncoding;
479     _headerLength += 3;
480   } else if (BytesEqual(bytes, 0xFE, 0xFF)) {
481     _contentEncoding = NSUTF16BigEndianStringEncoding;
482     _headerLength += 2;
483   } else if (BytesEqual(bytes, 0x00, 0x00, 0xFE, 0xFF)) {
484     _contentEncoding = NSUTF32BigEndianStringEncoding;
485     _headerLength += 4;
486   }
488   _completedByteOrderMarkCheck = YES;
491 - (void)checkForXMLDeclaration {
492   // WebKit interprets the byte order mark (BOM) as the truth and ignores and
493   // subsequent encodings. Thus if a BOM has already been found, there is no
494   // need to check for an XML declaration.
495   if (_headerLength)
496     _completedXMLDeclarationCheck = YES;
498   // Bail if we have completed this.
499   if (_completedXMLDeclarationCheck)
500     return;
502   // Do we already know the encoding?
503   if (_contentEncoding) {
504     _completedXMLDeclarationCheck = YES;
505     return;
506   }
508   // Bail if there is not yet enough data to check.
509   if (_pendingDataLength < kMinimumBytesNeededForXMLDecl)
510     return;
512   // It's possible that the server returned data in small chunks, so coalesce
513   // the data into one buffer if needed.
514   [self coalesceDataIfNeeded:kMinimumBytesNeededForXMLDecl];
516   // Get some data to investigate.
517   NSData* firstData = [_pendingData firstObject];
518   if (!firstData && [firstData length] < kMinimumBytesNeededForXMLDecl) {
519     NOTREACHED();
520     return;
521   }
523   // Do the same check that WebKit does for an XML declaration. The XML spec
524   // is not exactly clear about what, if anything, can appear before an XML
525   // declaration. Can there be white space? Can there be comments? WebKit only
526   // accepts XML declarations if they are right at the beginning of the content.
527   const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
528   if (BytesEqual(bytes, '<', '?', 'x', 'm', 'l')) {
529     _contentEncoding = NSISOLatin1StringEncoding;
530   } else if (BytesEqual(bytes, '<', 0, '?', 0, 'x', 0)) {
531     _contentEncoding = NSUTF16LittleEndianStringEncoding;
532   } else if (BytesEqual(bytes, 0, '<', 0, '?', 0, 'x')) {
533     _contentEncoding = NSUTF16BigEndianStringEncoding;
534   } else if (BytesEqual(bytes, '<', 0, 0, 0, '?', 0, 0, 0)) {
535     _contentEncoding = NSUTF32LittleEndianStringEncoding;
536   } else if (BytesEqual(bytes, 0, 0, 0, '<', 0, 0, 0, '?')) {
537     _contentEncoding = NSUTF32BigEndianStringEncoding;
538   }
540   _completedXMLDeclarationCheck = YES;
543 - (void)checkIfByteLimitPassed:(const WebCore::CharacterProvider&)provider {
544   // Put a hard limit on the number of characters we check before doing
545   // an injection.
546   if (provider.bytesProvided() >= kLimitOfBytesToCheckForHeadTag)
547     _completedCheckForWhereToInject = YES;
550 - (void)checkForWhereToInject {
551   // Bail if the check has already been completed.
552   if (_completedCheckForWhereToInject)
553     return;
555   // Bail if there is not yet enough data to check. We need a complete "<html "
556   // or a complete "<html>"
557   if (_pendingDataLength < kMinimumBytesNeededForHTMLTag)
558     return;
560   // It's possible that the server returned data in small chunks, so coalesce
561   // the data into one buffer if needed. Try pooling everything into one chunk
562   // that meets the limit of what we look at.
563   [self coalesceDataIfNeeded:kLimitOfBytesToCheckForHeadTag];
565   // Get some data to investigate. Of course we need at least
566   // kMinimumBytesNeededForHTMLTag bytes to do the investigation.
567   NSData* firstData = [_pendingData firstObject];
568   DCHECK([firstData length] >= kMinimumBytesNeededForHTMLTag);
570   const uint8* bytes8 = reinterpret_cast<const uint8*>([firstData bytes]);
572   WebCore::CharacterProvider provider;
573   switch (_contentEncoding) {
574     case NSUTF16BigEndianStringEncoding:
575     case NSUTF16LittleEndianStringEncoding:
576     case NSUTF32BigEndianStringEncoding:
577     case NSUTF32LittleEndianStringEncoding: {
578       const uint16* bytes16 =
579           reinterpret_cast<const uint16*>(bytes8 + _headerLength);
581       provider.setContents(bytes16, [firstData length] - _headerLength);
583       if (_contentEncoding == NSUTF16LittleEndianStringEncoding ||
584           _contentEncoding == NSUTF32LittleEndianStringEncoding)
585         provider.setLittleEndian();
586       break;
587     }
589     default: {
590       provider.setContents(bytes8 + _headerLength,
591                            [firstData length] - _headerLength);
592       break;
593     }
594   }
596   WebCore::HTMLTokenizer tokenizer;
597   WebCore::HTMLToken token;
598   while(!_completedCheckForWhereToInject &&
599         tokenizer.nextToken(provider, token)) {
600     WebCore::HTMLToken::Type tokenType = token.type();
601     switch (tokenType) {
602       case WebCore::HTMLToken::StartTag: {
603         DEFINE_STATIC_LOCAL_STRING(htmlTag, "html");
605         // If any start tag is seen, the check is complete.
606         _proceedWithInjection = YES;
607         _completedCheckForWhereToInject = YES;
609         // We always have to account for any BOM header when injecting.
610         _byteOffsetAtWhichToInject = _headerLength;
612         if (token.nameEquals(htmlTag, htmlTagLength)) {
613           // There is an html tag, so the insertion needs
614           // to happen after this start tag.
615           _byteOffsetAtWhichToInject += provider.bytesProvided();
616         }
618         break;
619       }
621       default: {
622         [self checkIfByteLimitPassed:provider];
623         token.clear();
624         break;
625       }
626     }
627   }
629   // There is an early exit case right at the end of the start-tag from the
630   // WebCore::HTMLTokenizer::nextToken(), so double check to see if we hit
631   // the limit.
632   [self checkIfByteLimitPassed:provider];
635 @end