Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h" | 5 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h" |
| 6 | 6 |
| 7 #import <WebKit/WebKit.h> | 7 #import <WebKit/WebKit.h> |
| 8 | 8 |
| 9 #include "base/ios/ios_util.h" | 9 #include "base/ios/ios_util.h" |
| 10 #include "base/ios/weak_nsobject.h" | 10 #include "base/ios/weak_nsobject.h" |
| 11 #include "base/json/json_reader.h" | 11 #include "base/json/json_reader.h" |
| 12 #include "base/json/string_escape.h" | |
| 12 #import "base/mac/scoped_nsobject.h" | 13 #import "base/mac/scoped_nsobject.h" |
| 13 #include "base/macros.h" | 14 #include "base/macros.h" |
| 14 #include "base/strings/sys_string_conversions.h" | 15 #include "base/strings/sys_string_conversions.h" |
| 15 #include "base/values.h" | 16 #include "base/values.h" |
| 16 #import "ios/net/http_response_headers_util.h" | 17 #import "ios/net/http_response_headers_util.h" |
| 17 #import "ios/web/crw_network_activity_indicator_manager.h" | 18 #import "ios/web/crw_network_activity_indicator_manager.h" |
| 18 #import "ios/web/navigation/crw_session_controller.h" | 19 #import "ios/web/navigation/crw_session_controller.h" |
| 19 #import "ios/web/navigation/crw_session_entry.h" | 20 #import "ios/web/navigation/crw_session_entry.h" |
| 20 #include "ios/web/navigation/navigation_item_impl.h" | 21 #include "ios/web/navigation/navigation_item_impl.h" |
| 21 #include "ios/web/navigation/web_load_params.h" | 22 #include "ios/web/navigation/web_load_params.h" |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 46 #import "net/base/mac/url_conversions.h" | 47 #import "net/base/mac/url_conversions.h" |
| 47 #include "net/ssl/ssl_info.h" | 48 #include "net/ssl/ssl_info.h" |
| 48 #include "url/url_constants.h" | 49 #include "url/url_constants.h" |
| 49 | 50 |
| 50 namespace { | 51 namespace { |
| 51 // Extracts Referer value from WKNavigationAction request header. | 52 // Extracts Referer value from WKNavigationAction request header. |
| 52 NSString* GetRefererFromNavigationAction(WKNavigationAction* action) { | 53 NSString* GetRefererFromNavigationAction(WKNavigationAction* action) { |
| 53 return [action.request valueForHTTPHeaderField:@"Referer"]; | 54 return [action.request valueForHTTPHeaderField:@"Referer"]; |
| 54 } | 55 } |
| 55 | 56 |
| 57 // Escapes characters and encloses given string in quotes for use in JavaScript. | |
| 58 NSString* EscapeAndQuoteStringForJavaScript(NSString* unescapedString) { | |
| 59 std::string string = base::SysNSStringToUTF8(unescapedString); | |
| 60 return base::SysUTF8ToNSString(base::GetQuotedJSONString(string)); | |
|
stuartmorgan
2015/10/01 23:36:27
The round-tripping of what's going to be a really
stkhapugin
2015/10/08 16:57:24
I can't find a way to do it directly on NSString.
| |
| 61 } | |
| 62 | |
| 56 NSString* const kScriptMessageName = @"crwebinvoke"; | 63 NSString* const kScriptMessageName = @"crwebinvoke"; |
| 57 NSString* const kScriptImmediateName = @"crwebinvokeimmediate"; | 64 NSString* const kScriptImmediateName = @"crwebinvokeimmediate"; |
| 58 | 65 |
| 66 // JavaScript template to do a POST request using an XMLHttpRequest. | |
| 67 // It takes three arguments (in order): | |
| 68 // * The quoted and escaped URL to send a POST request to. | |
| 69 // * The HTTP headers of the request. They should be written as valid JavaScript | |
| 70 // statements, adding headers to the XMLHttpRequest variable named 'req' | |
| 71 // (e.g. 'req.setRequestHeader("Foo", "Bar");'). | |
| 72 // * The base64 string of POST request body. | |
| 73 // * Content-Type string | |
| 74 NSString* const kPostRequestTemplate = | |
| 75 @"<html><script>" | |
|
stuartmorgan
2015/10/01 23:36:27
I'm with Eugene; this is sizable chunk of JS, and
stkhapugin
2015/10/08 16:57:24
Done.
| |
| 76 "function b64ToBlob(b64Data, contentType) {" | |
| 77 " contentType = contentType || '';" | |
| 78 " var sliceSize = 512;" | |
| 79 " var byteCharacters = b64Data;" | |
| 80 " var byteArrays = [];" | |
| 81 " for (var offset = 0; offset < byteCharacters.length; offset += " | |
| 82 "sliceSize) {" | |
| 83 " var slice = byteCharacters.slice(offset, offset + sliceSize);" | |
| 84 " var byteNumbers = new Array(slice.length);" | |
| 85 " for (var i = 0; i < slice.length; i++) {" | |
| 86 " byteNumbers[i] = slice.charCodeAt(i);" | |
| 87 " }" | |
| 88 " var byteArray = new Uint8Array(byteNumbers);" | |
| 89 " byteArrays.push(byteArray);" | |
| 90 " }" | |
| 91 " var blob = new Blob(byteArrays, {type: contentType});" | |
| 92 " return blob;" | |
| 93 "}" | |
| 94 " function createAndSendPostRequest() {" | |
| 95 " var req = new XMLHttpRequest();" | |
| 96 " req.open(\"POST\", %@, false);" | |
| 97 " %@" //< This sets request headers. | |
| 98 " var blob = b64ToBlob(atob(%@), %@);" //< base64 string and | |
| 99 // content type | |
| 100 " req.send(blob);" | |
| 101 " if (req.status != 200) {" | |
| 102 " throw req.status;" | |
| 103 " }" | |
| 104 " return req.responseText;" | |
| 105 " }" | |
| 106 " " | |
| 107 " document.open();" | |
| 108 " document.write(createAndSendPostRequest());" | |
| 109 " document.close();" | |
| 110 | |
| 111 "</script></html>"; | |
| 112 | |
| 59 // Utility functions for storing the source of NSErrors received by WKWebViews: | 113 // Utility functions for storing the source of NSErrors received by WKWebViews: |
| 60 // - Errors received by |-webView:didFailProvisionalNavigation:withError:| are | 114 // - Errors received by |-webView:didFailProvisionalNavigation:withError:| are |
| 61 // recorded using WKWebViewErrorSource::PROVISIONAL_LOAD. These should be | 115 // recorded using WKWebViewErrorSource::PROVISIONAL_LOAD. These should be |
| 62 // aborted. | 116 // aborted. |
| 63 // - Errors received by |-webView:didFailNavigation:withError:| are recorded | 117 // - Errors received by |-webView:didFailNavigation:withError:| are recorded |
| 64 // using WKWebViewsource::NAVIGATION. These errors should not be aborted, as | 118 // using WKWebViewsource::NAVIGATION. These errors should not be aborted, as |
| 65 // the WKWebView will automatically retry the load. | 119 // the WKWebView will automatically retry the load. |
| 66 NSString* const kWKWebViewErrorSourceKey = @"ErrorSource"; | 120 NSString* const kWKWebViewErrorSourceKey = @"ErrorSource"; |
| 67 typedef enum { NONE = 0, PROVISIONAL_LOAD, NAVIGATION } WKWebViewErrorSource; | 121 typedef enum { NONE = 0, PROVISIONAL_LOAD, NAVIGATION } WKWebViewErrorSource; |
| 68 NSError* WKWebViewErrorWithSource(NSError* error, WKWebViewErrorSource source) { | 122 NSError* WKWebViewErrorWithSource(NSError* error, WKWebViewErrorSource source) { |
| (...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 162 @property(nonatomic, readonly) NSString* requestGroupIDForUserAgent; | 216 @property(nonatomic, readonly) NSString* requestGroupIDForUserAgent; |
| 163 | 217 |
| 164 // Activity indicator group ID for this web controller. | 218 // Activity indicator group ID for this web controller. |
| 165 @property(nonatomic, readonly) NSString* activityIndicatorGroupID; | 219 @property(nonatomic, readonly) NSString* activityIndicatorGroupID; |
| 166 | 220 |
| 167 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) | 221 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) |
| 168 // Identifier used for storing and retrieving certificates. | 222 // Identifier used for storing and retrieving certificates. |
| 169 @property(nonatomic, readonly) int certGroupID; | 223 @property(nonatomic, readonly) int certGroupID; |
| 170 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) | 224 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) |
| 171 | 225 |
| 226 // Constructs an HTML page that executes the request through JavaScript and | |
| 227 // replaces document with the result. | |
| 228 - (NSString*)HTMLForPOSTRequest:(NSURLRequest*)request; | |
| 229 | |
| 230 // Loads POST request with body in |_wkWebView| by constructing an HTML page | |
| 231 // that executes the request through javascript and replaces document with the | |
| 232 // result. | |
| 233 // Note that this approach includes multiple body encodings and decodings, plus | |
| 234 // the data is passed to |_wkWebView| on main thread, hence the performance is | |
| 235 // expected to be low. | |
| 236 // This is necessary because WKWebView ignores POST request body. | |
| 237 // Workaround for https://bugs.webkit.org/show_bug.cgi?id=145410 | |
| 238 - (void)loadPOSTRequestWithBody:(NSMutableURLRequest*)request; | |
| 239 | |
| 172 // Returns the WKWebViewConfigurationProvider associated with the web | 240 // Returns the WKWebViewConfigurationProvider associated with the web |
| 173 // controller's BrowserState. | 241 // controller's BrowserState. |
| 174 - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider; | 242 - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider; |
| 175 | 243 |
| 176 // Creates a web view with given |config|. No-op if web view is already created. | 244 // Creates a web view with given |config|. No-op if web view is already created. |
| 177 - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config; | 245 - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config; |
| 178 | 246 |
| 179 // Returns a new autoreleased web view created with given configuration. | 247 // Returns a new autoreleased web view created with given configuration. |
| 180 - (WKWebView*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config; | 248 - (WKWebView*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config; |
| 181 | 249 |
| (...skipping 259 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 441 return web::WEB_VIEW_DOCUMENT_TYPE_HTML; | 509 return web::WEB_VIEW_DOCUMENT_TYPE_HTML; |
| 442 } | 510 } |
| 443 | 511 |
| 444 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC; | 512 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC; |
| 445 } | 513 } |
| 446 | 514 |
| 447 - (void)loadRequest:(NSMutableURLRequest*)request { | 515 - (void)loadRequest:(NSMutableURLRequest*)request { |
| 448 [_wkWebView loadRequest:request]; | 516 [_wkWebView loadRequest:request]; |
| 449 } | 517 } |
| 450 | 518 |
| 519 - (NSString*)HTMLForPOSTRequest:(NSURLRequest*)request { | |
|
Eugene But (OOO till 7-30)
2015/09/30 15:45:16
NIT: Please move these methods before "webViewConf
stkhapugin
2015/10/08 16:57:24
Done.
| |
| 520 NSString* base64Data = [[request HTTPBody] base64EncodedStringWithOptions:0]; | |
| 521 NSString* originURL = [[request URL] absoluteString]; | |
| 522 NSString* contentType = nil; | |
| 523 NSMutableString* headerString = [NSMutableString string]; | |
| 524 for (NSString* headerField in [[request allHTTPHeaderFields] allKeys]) { | |
| 525 NSString* value = [request allHTTPHeaderFields][headerField]; | |
| 526 if ([headerField isEqualToString:@"Content-Type"]) { | |
| 527 contentType = value; | |
| 528 } | |
| 529 [headerString appendFormat:@"req.setRequestHeader(%@, %@);", | |
| 530 EscapeAndQuoteStringForJavaScript(headerField), | |
| 531 EscapeAndQuoteStringForJavaScript(value)]; | |
| 532 } | |
| 533 | |
| 534 return [NSString stringWithFormat:kPostRequestTemplate, | |
| 535 EscapeAndQuoteStringForJavaScript(originURL), | |
| 536 headerString, | |
| 537 EscapeAndQuoteStringForJavaScript(base64Data), | |
| 538 EscapeAndQuoteStringForJavaScript(contentType)]; | |
| 539 } | |
| 540 | |
| 541 - (void)loadPOSTRequestWithBody:(NSMutableURLRequest*)request { | |
| 542 NSString* HTML = [self HTMLForPOSTRequest:request]; | |
| 543 [_wkWebView loadHTMLString:HTML baseURL:request.URL]; | |
| 544 } | |
| 545 | |
| 451 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL { | 546 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL { |
| 452 [_wkWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)]; | 547 [_wkWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)]; |
| 453 } | 548 } |
| 454 | 549 |
| 455 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass | 550 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass |
| 456 presenceBeacon:(NSString*)beacon { | 551 presenceBeacon:(NSString*)beacon { |
| 457 return [_injectedScriptManagers containsObject:jsInjectionManagerClass]; | 552 return [_injectedScriptManagers containsObject:jsInjectionManagerClass]; |
| 458 } | 553 } |
| 459 | 554 |
| 460 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass { | 555 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass { |
| 461 // Skip evaluation if there's no content (e.g., if what's being injected is | 556 // Skip evaluation if there's no content (e.g., if what's being injected is |
| 462 // an umbrella manager). | 557 // an umbrella manager). |
| 463 if ([script length]) { | 558 if ([script length]) { |
| 464 [super injectScript:script forClass:JSInjectionManagerClass]; | 559 [super injectScript:script forClass:JSInjectionManagerClass]; |
| 465 // Every injection except windowID requires windowID check. | 560 // Every injection except windowID requires windowID check. |
| 466 if (JSInjectionManagerClass != [CRWJSWindowIdManager class]) | 561 if (JSInjectionManagerClass != [CRWJSWindowIdManager class]) |
| 467 script = [self scriptByAddingWindowIDCheckForScript:script]; | 562 script = [self scriptByAddingWindowIDCheckForScript:script]; |
| 468 web::EvaluateJavaScript(_wkWebView, script, nil); | 563 web::EvaluateJavaScript(_wkWebView, script, nil); |
| 469 } | 564 } |
| 470 [_injectedScriptManagers addObject:JSInjectionManagerClass]; | 565 [_injectedScriptManagers addObject:JSInjectionManagerClass]; |
| 471 } | 566 } |
| 472 | 567 |
| 473 - (void)willLoadCurrentURLInWebView { | 568 - (void)willLoadCurrentURLInWebView { |
| 474 // TODO(stuartmorgan): Get a WKWebView version of the request ID verification | 569 // TODO(stuartmorgan): Get a WKWebView version of the request ID verification |
| 475 // code working for debug builds. | 570 // code working for debug builds. |
| 476 } | 571 } |
| 477 | 572 |
| 478 - (void)loadRequestForCurrentNavigationItem { | 573 - (void)loadRequestForCurrentNavigationItem { |
| 479 DCHECK(self.webView && !self.nativeController); | 574 DCHECK(self.webView && !self.nativeController); |
| 575 DCHECK([self currentSessionEntry]); | |
| 576 | |
| 577 web::WKBackForwardListItemHolder* holder = | |
| 578 [self currentBackForwardListItemHolder]; | |
| 579 BOOL isFormResubmission = | |
| 580 (holder->navigation_type() == WKNavigationTypeFormResubmitted || | |
| 581 holder->navigation_type() == WKNavigationTypeFormSubmitted); | |
| 582 web::NavigationItemImpl* currentItem = | |
| 583 [self currentSessionEntry].navigationItemImpl; | |
| 584 NSData* POSTData = currentItem->GetPostData(); | |
| 585 NSMutableURLRequest* request = [self requestForCurrentNavigationItem]; | |
| 480 | 586 |
| 481 ProceduralBlock defaultNavigationBlock = ^{ | 587 ProceduralBlock defaultNavigationBlock = ^{ |
| 482 [self registerLoadRequest:[self currentNavigationURL] | 588 [self registerLoadRequest:[self currentNavigationURL] |
| 483 referrer:[self currentSessionEntryReferrer] | 589 referrer:[self currentSessionEntryReferrer] |
| 484 transition:[self currentTransition]]; | 590 transition:[self currentTransition]]; |
| 485 [self loadRequest:[self requestForCurrentNavigationItem]]; | 591 [self loadRequest:request]; |
| 486 }; | 592 }; |
| 487 | 593 |
| 488 // If there is no corresponding WKBackForwardListItem, or the item is not in | 594 // If the request has POST data and is not a form resubmission, configure and |
|
Eugene But (OOO till 7-30)
2015/09/30 15:45:16
This comment should be at line 621, not here.
stkhapugin
2015/10/08 16:57:24
Done.
| |
| 489 // the current WKWebView's back-forward list, navigating using WKWebView API | 595 // run the POST request. |
| 490 // is not possible. In this case, fall back to the default navigation | 596 ProceduralBlock POSTBlock = ^{ |
| 491 // mechanism. | 597 [request setHTTPMethod:@"POST"]; |
| 492 web::WKBackForwardListItemHolder* holder = | 598 [request setHTTPBody:POSTData]; |
| 493 [self currentBackForwardListItemHolder]; | 599 [request setAllHTTPHeaderFields:[self currentHTTPHeaders]]; |
| 494 if (!holder->back_forward_list_item() || | 600 [self registerLoadRequest:[self currentNavigationURL] |
| 495 ![self isBackForwardListItemValid:holder->back_forward_list_item()]) { | 601 referrer:[self currentSessionEntryReferrer] |
| 496 defaultNavigationBlock(); | 602 transition:[self currentTransition]]; |
| 497 return; | 603 [self loadPOSTRequestWithBody:request]; |
| 498 } | 604 }; |
| 499 | 605 |
| 500 ProceduralBlock webViewNavigationBlock = ^{ | 606 ProceduralBlock webViewNavigationBlock = ^{ |
| 501 // If the current navigation URL is the same as the URL of the visible | 607 // If the current navigation URL is the same as the URL of the visible |
| 502 // page, that means the user requested a reload. |goToBackForwardListItem| | 608 // page, that means the user requested a reload. |goToBackForwardListItem| |
| 503 // will be a no-op when it is passed the current back forward list item, | 609 // will be a no-op when it is passed the current back forward list item, |
| 504 // so |reload| must be explicitly called. | 610 // so |reload| must be explicitly called. |
| 505 [self registerLoadRequest:[self currentNavigationURL] | 611 [self registerLoadRequest:[self currentNavigationURL] |
| 506 referrer:[self currentSessionEntryReferrer] | 612 referrer:[self currentSessionEntryReferrer] |
| 507 transition:[self currentTransition]]; | 613 transition:[self currentTransition]]; |
| 508 if ([self currentNavigationURL] == net::GURLWithNSURL([_wkWebView URL])) { | 614 if ([self currentNavigationURL] == net::GURLWithNSURL([_wkWebView URL])) { |
| 509 [_wkWebView reload]; | 615 [_wkWebView reload]; |
| 510 } else { | 616 } else { |
| 511 [_wkWebView goToBackForwardListItem:holder->back_forward_list_item()]; | 617 [_wkWebView goToBackForwardListItem:holder->back_forward_list_item()]; |
| 512 } | 618 } |
| 513 }; | 619 }; |
| 514 | 620 |
| 621 if (POSTData.length && !isFormResubmission) { | |
| 622 POSTBlock(); | |
| 623 return; | |
| 624 } | |
| 625 | |
| 626 // If there is no corresponding WKBackForwardListItem, or the item is not in | |
| 627 // the current WKWebView's back-forward list, navigating using WKWebView API | |
| 628 // is not possible. In this case, fall back to the default navigation | |
| 629 // mechanism. | |
| 630 if (!holder->back_forward_list_item() || | |
| 631 ![self isBackForwardListItemValid:holder->back_forward_list_item()]) { | |
| 632 defaultNavigationBlock(); | |
| 633 return; | |
| 634 } | |
| 635 | |
| 515 // If the request is not a form submission or resubmission, or the user | 636 // If the request is not a form submission or resubmission, or the user |
| 516 // doesn't need to confirm the load, then continue right away. | 637 // doesn't need to confirm the load, then continue right away. |
| 517 web::NavigationItemImpl* currentItem = | 638 if (!isFormResubmission || |
| 518 [self currentSessionEntry].navigationItemImpl; | |
| 519 if ((holder->navigation_type() != WKNavigationTypeFormResubmitted && | |
| 520 holder->navigation_type() != WKNavigationTypeFormSubmitted) || | |
| 521 currentItem->ShouldSkipResubmitDataConfirmation()) { | 639 currentItem->ShouldSkipResubmitDataConfirmation()) { |
| 522 webViewNavigationBlock(); | 640 webViewNavigationBlock(); |
| 523 return; | 641 return; |
| 524 } | 642 } |
| 525 | 643 |
| 526 // If the request is form submission or resubmission, then prompt the | 644 // If the request is form submission or resubmission, then prompt the |
| 527 // user before proceeding. | 645 // user before proceeding. |
| 646 DCHECK(isFormResubmission); | |
| 528 [self.delegate webController:self | 647 [self.delegate webController:self |
| 529 onFormResubmissionForRequest:nil | 648 onFormResubmissionForRequest:nil |
| 530 continueBlock:webViewNavigationBlock | 649 continueBlock:webViewNavigationBlock |
| 531 cancelBlock:defaultNavigationBlock]; | 650 cancelBlock:defaultNavigationBlock]; |
| 532 } | 651 } |
| 533 | 652 |
| 534 // Overrides the hashchange workaround in the super class that manually | 653 // Overrides the hashchange workaround in the super class that manually |
| 535 // triggers Javascript hashchange events. If navigating with native API, | 654 // triggers Javascript hashchange events. If navigating with native API, |
| 536 // i.e. using a back forward list item, hashchange events will be triggered | 655 // i.e. using a back forward list item, hashchange events will be triggered |
| 537 // automatically, so no URL tampering is required. | 656 // automatically, so no URL tampering is required. |
| (...skipping 948 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1486 placeholderText:defaultText | 1605 placeholderText:defaultText |
| 1487 requestURL: | 1606 requestURL: |
| 1488 net::GURLWithNSURL(frame.request.URL) | 1607 net::GURLWithNSURL(frame.request.URL) |
| 1489 completionHandler:completionHandler]; | 1608 completionHandler:completionHandler]; |
| 1490 } else if (completionHandler) { | 1609 } else if (completionHandler) { |
| 1491 completionHandler(nil); | 1610 completionHandler(nil); |
| 1492 } | 1611 } |
| 1493 } | 1612 } |
| 1494 | 1613 |
| 1495 @end | 1614 @end |
| OLD | NEW |