diff --git a/.vscode/settings.json b/.vscode/settings.json index 4295a933d1f..1e2e92eba7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,7 @@ "**/lib/**/*.js": true, "**/dist/**/*.js": true }, - "editor.formatOnSave": true, + "editor.formatOnSave": false, "eslint.format.enable": true, "eslint.packageManager": "yarn", "eslint.enable": true, diff --git a/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json new file mode 100644 index 00000000000..cda201f8577 --- /dev/null +++ b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Allow fetching HTTP content by segments", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json index 65a0a651fd4..ca338fe1574 100644 --- a/packages/@react-native-windows/tester/overrides.json +++ b/packages/@react-native-windows/tester/overrides.json @@ -14,6 +14,14 @@ "baseHash": "a1d1c6638e815f6dd53504983813fda92f3b5478", "issue": 6341 }, + { + "type": "platform", + "file": "src/js/examples/HTTP/HTTPExample.js" + }, + { + "type": "platform", + "file": "src/js/examples/HTTP/HTTPExampleMultiPartFormData.js" + }, { "type": "patch", "file": "src/js/examples/Pressable/PressableExample.windows.js", @@ -48,4 +56,4 @@ "baseHash": "d12b4947135ada2dcb1c68835f53d7f0beff8a4e" } ] -} \ No newline at end of file +} diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js new file mode 100644 index 00000000000..95311de2d10 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @flow + * @format + */ +'use strict'; + +const React = require('react'); +const HTTPExampleMultiPartFormData = require('./HTTPExampleMultiPartFormData'); +const XHRExampleFetch = require('../XHR/XHRExampleFetch'); + +exports.framework = 'React'; +exports.title = 'HTTP'; +exports.category = 'Basic'; +exports.description = + ('Example that demonstrates direct and indirect HTTP native module usage.': string); +exports.examples = [ + { + title: 'multipart/form-data POST', + render(): React.Node { + return ; + }, + }, + { + title: 'Fetch Test', + render(): React.Node { + return ; + }, + }, +]; diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js new file mode 100644 index 00000000000..9f80487bf32 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js @@ -0,0 +1,164 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @flow + * @format + */ +'use strict'; + +const React = require('react'); + +const {StyleSheet, Text, TextInput, View} = require('react-native'); + +/** + * See https://www.w3schools.com/php/php_form_validation.asp + */ +class HTTPExampleMultiPartFormData extends React.Component { + responseURL: ?string; + responseHeaders: ?Object; + + constructor(props: any) { + super(props); + this.state = { + responseText: null, + }; + this.responseURL = null; + this.responseHeaders = null; + } + + submit(uri: string) { + const formData = new FormData(); + + formData.append('name', { + string: 'Name', + type: 'application/text', + }); + formData.append('email', { + string: 'me@mail.com', + type: 'application/text', + }); + formData.append('website', { + string: 'http://aweb.com', + type: 'application/text', + }); + formData.append('comment', { + string: 'Hello', + type: 'application/text', + }); + formData.append('gender', { + string: 'Other', + type: 'application/text', + }); + + fetch(uri, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: formData, + }) + .then(response => { + this.responseURL = response.url; + this.responseHeaders = response.headers; + + return response.text(); + }) + .then(body => { + this.setState({responseText: body}); + }); + } + + _renderHeaders(): null | Array { + if (!this.responseHeaders) { + return null; + } + + const responseHeaders = []; + const keys = Object.keys(this.responseHeaders.map); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + // $FlowFixMe[incompatible-use] + const value = this.responseHeaders.get(key); + responseHeaders.push( + + {key}: {value} + , + ); + } + return responseHeaders; + } + + render(): React.Node { + const responseURL = this.responseURL ? ( + + Server response URL: + {this.responseURL} + + ) : null; + + const responseHeaders = this.responseHeaders ? ( + + Server response headers: + {this._renderHeaders()} + + ) : null; + + const response = this.state.responseText ? ( + + Server response: + + + ) : null; + + return ( + + Edit URL to submit: + { + this.submit(event.nativeEvent.text); + }} + style={styles.textInput} + /> + {responseURL} + {responseHeaders} + {response} + + ); + } +} + +const styles = StyleSheet.create({ + textInput: { + flex: 1, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 30, + paddingLeft: 8, + }, + label: { + flex: 1, + color: '#aaa', + fontWeight: '500', + height: 20, + }, + textOutput: { + flex: 1, + fontSize: 17, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 200, + paddingLeft: 8, + }, +}); + +module.exports = HTTPExampleMultiPartFormData; diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index a450e5df622..f9b6d45246c 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -12,6 +12,14 @@ import type {RNTesterModuleInfo} from '../types/RNTesterTypes'; import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags'; const Components: Array = [ + { + key: 'HTTPExample', + module: require('../examples/HTTP/HTTPExample'), + }, + { + key: 'XHRExample', + module: require('../examples/XHR/XHRExample'), + }, { key: 'ActivityIndicatorExample', category: 'UI', diff --git a/vnext/Desktop.DLL/react-native-win32.x64.def b/vnext/Desktop.DLL/react-native-win32.x64.def index f8d5742909f..eec2862ff77 100644 --- a/vnext/Desktop.DLL/react-native-win32.x64.def +++ b/vnext/Desktop.DLL/react-native-win32.x64.def @@ -56,7 +56,6 @@ EXPORTS ?makeChakraRuntime@JSI@Microsoft@@YA?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QEAUChakraRuntimeArgs@12@@Z ?Make@IHttpResource@Networking@React@Microsoft@@SA?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ ?CreateTimingModule@react@facebook@@YA?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@AEBV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z -??0NetworkingModule@React@Microsoft@@QEAA@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YA?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ ?Hash128@SpookyHashV2@hash@folly@@SAXPEBX_KPEA_K2@Z ??1Instance@react@facebook@@QEAA@XZ diff --git a/vnext/Desktop.DLL/react-native-win32.x86.def b/vnext/Desktop.DLL/react-native-win32.x86.def index d899f933715..f9b316cb1b0 100644 --- a/vnext/Desktop.DLL/react-native-win32.x86.def +++ b/vnext/Desktop.DLL/react-native-win32.x86.def @@ -52,7 +52,6 @@ EXPORTS ?makeChakraRuntime@JSI@Microsoft@@YG?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QAUChakraRuntimeArgs@12@@Z ?CreateTimingModule@react@facebook@@YG?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@ABV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z ?Make@IHttpResource@Networking@React@Microsoft@@SG?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ -??0NetworkingModule@React@Microsoft@@QAE@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YG?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ ?Hash128@SpookyHashV2@hash@folly@@SGXPBXIPA_K1@Z ?assertionFailure@detail@folly@@YGXPBD00I0H@Z diff --git a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp index 0813355bf0a..4ed441749b3 100644 --- a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp @@ -27,7 +27,6 @@ TEST_MODULE_INITIALIZE(InitModule) { SetRuntimeOptionBool("WebSocket.AcceptSelfSigned", true); SetRuntimeOptionBool("UseBeastWebSocket", false); - SetRuntimeOptionBool("Http.UseMonolithicModule", false); SetRuntimeOptionBool("Blob.EnableModule", true); // WebSocketJSExecutor can't register native log hooks. diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index 9522dc4b59a..cc0cfc04438 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -19,12 +19,8 @@ namespace Microsoft::ReactNative { -using winrt::Microsoft::ReactNative::ReactPropertyBag; - namespace { -using winrt::Microsoft::ReactNative::ReactPropertyId; - bool HasPackageIdentity() noexcept { static const bool hasPackageIdentity = []() noexcept { auto packageStatics = winrt::get_activation_factory( @@ -39,13 +35,6 @@ bool HasPackageIdentity() noexcept { return hasPackageIdentity; } -ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { - static ReactPropertyId propId{ - L"ReactNative.Http" - L"UseMonolithicModule"}; - return propId; -} - } // namespace std::vector GetCoreModules( @@ -61,17 +50,15 @@ std::vector GetCoreModules( [props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); }, jsMessageQueue); - if (!ReactPropertyBag(context->Properties()).Get(HttpUseMonolithicModuleProperty())) { - modules.emplace_back( - Microsoft::React::GetBlobModuleName(), - [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); }, - batchingUIMessageQueue); - - modules.emplace_back( - Microsoft::React::GetFileReaderModuleName(), - [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); }, - batchingUIMessageQueue); - } + modules.emplace_back( + Microsoft::React::GetBlobModuleName(), + [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); }, + batchingUIMessageQueue); + + modules.emplace_back( + Microsoft::React::GetFileReaderModuleName(), + [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); }, + batchingUIMessageQueue); modules.emplace_back( "Timing", diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp index 9f7919e31fc..67cb040b52e 100644 --- a/vnext/Shared/Modules/FileReaderModule.cpp +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -4,6 +4,7 @@ #include "FileReaderModule.h" #include +#include // Boost Library #include diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index 20f4ea0fd04..326f677d843 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -33,8 +33,10 @@ constexpr char moduleName[] = "Networking"; // React event names constexpr char completedResponse[] = "didCompleteNetworkResponse"; constexpr char receivedResponse[] = "didReceiveNetworkResponse"; -constexpr char receivedData[] = "didReceiveNetworkData"; +constexpr char sentData[] = "didSendNetworkData"; +constexpr char receivedIncrementalData[] = "didReceiveNetworkIncrementalData"; constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; +constexpr char receivedData[] = "didReceiveNetworkData"; static void SetUpHttpResource( shared_ptr resource, @@ -60,9 +62,6 @@ static void SetUpHttpResource( resource->SetOnData([weakReactInstance](int64_t requestId, string &&responseData) { SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); - - // TODO: Move into separate method IF not executed right after onData() - SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); }); // Explicitly declaring function type to avoid type inference ambiguity. @@ -72,6 +71,22 @@ static void SetUpHttpResource( }; resource->SetOnData(std::move(onDataDynamic)); + resource->SetOnIncrementalData( + [weakReactInstance](int64_t requestId, string &&responseData, int64_t progress, int64_t total) { + SendEvent( + weakReactInstance, + receivedIncrementalData, + dynamic::array(requestId, std::move(responseData), progress, total)); + }); + + resource->SetOnDataProgress([weakReactInstance](int64_t requestId, int64_t progress, int64_t total) { + SendEvent(weakReactInstance, receivedDataProgress, dynamic::array(requestId, progress, total)); + }); + + resource->SetOnResponseComplete([weakReactInstance](int64_t requestId) { + SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); + }); + resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) { dynamic args = dynamic::array(requestId, std::move(message)); if (isTimeout) { @@ -108,90 +123,90 @@ std::map HttpModule::getConstants() { } // clang-format off -std::vector HttpModule::getMethods() { + std::vector HttpModule::getMethods() { - return - { + return { - "sendRequest", - [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback) { - auto holder = weakHolder.lock(); - if (!holder) { - return; - } - - auto resource = holder->Module->m_resource; - if (!holder->Module->m_isResourceSetup) + "sendRequest", + [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback) { - SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); - holder->Module->m_isResourceSetup = true; - } + auto holder = weakHolder.lock(); + if (!holder) { + return; + } - auto params = facebook::xplat::jsArgAsObject(args, 0); - IHttpResource::Headers headers; - for (auto& header : params["headers"].items()) { - headers.emplace(header.first.getString(), header.second.getString()); - } + auto resource = holder->Module->m_resource; + if (!holder->Module->m_isResourceSetup) + { + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; + } - resource->SendRequest( - params["method"].asString(), - params["url"].asString(), - params["requestId"].asInt(), - std::move(headers), - std::move(params["data"]), - params["responseType"].asString(), - params["incrementalUpdates"].asBool(), - static_cast(params["timeout"].asDouble()), - params["withCredentials"].asBool(), - [cxxCallback = std::move(cxxCallback)](int64_t requestId) { - cxxCallback({requestId}); + auto params = facebook::xplat::jsArgAsObject(args, 0); + IHttpResource::Headers headers; + for (auto& header : params["headers"].items()) { + headers.emplace(header.first.getString(), header.second.getString()); } - ); - } - }, - { - "abortRequest", - [weakHolder = weak_ptr(m_holder)](dynamic args) + + resource->SendRequest( + params["method"].asString(), + params["url"].asString(), + params["requestId"].asInt(), + std::move(headers), + std::move(params["data"]), + params["responseType"].asString(), + params["incrementalUpdates"].asBool(), + static_cast(params["timeout"].asDouble()), + params["withCredentials"].asBool(), + [cxxCallback = std::move(cxxCallback)](int64_t requestId) { + cxxCallback({requestId}); + } + ); + } + }, { - auto holder = weakHolder.lock(); - if (!holder) + "abortRequest", + [weakHolder = weak_ptr(m_holder)](dynamic args) { - return; - } + auto holder = weakHolder.lock(); + if (!holder) + { + return; + } - auto resource = holder->Module->m_resource; - if (!holder->Module->m_isResourceSetup) - { - SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); - holder->Module->m_isResourceSetup = true; - } + auto resource = holder->Module->m_resource; + if (!holder->Module->m_isResourceSetup) + { + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; + } - resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0)); - } - }, - { - "clearCookies", - [weakHolder = weak_ptr(m_holder)](dynamic args) + resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0)); + } + }, { - auto holder = weakHolder.lock(); - if (!holder) + "clearCookies", + [weakHolder = weak_ptr(m_holder)](dynamic args) { - return; - } + auto holder = weakHolder.lock(); + if (!holder) + { + return; + } - auto resource = holder->Module->m_resource; - if (!holder->Module->m_isResourceSetup) - { - SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); - holder->Module->m_isResourceSetup = true; - } + auto resource = holder->Module->m_resource; + if (!holder->Module->m_isResourceSetup) + { + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; + } - resource->ClearCookies(); + resource->ClearCookies(); + } } - } - }; -} + }; + } // clang-format on #pragma endregion CxxModule diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index 17d791d1868..c590e91aac7 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -91,10 +91,136 @@ struct IHttpResource { virtual void ClearCookies() noexcept = 0; + /// + /// Sets a function to be invoked when a request has been successfully responded. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response arrives and its headers are received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Object containing basic response data + /// + /// virtual void SetOnResponse(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Response content payload (plain text or Base64-encoded) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Structured response content payload (i.e. Blob data) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response content increment has been received. + /// + /// + /// The handler set by this method will only be called if the request sets the incremental updates flag. + /// The handler is also mutually exclusive with those set by `SetOnData`, which are used for one pass, non-incremental + /// updates. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Partial response content data increment (non-accumulative) + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnIncrementalData( + std::function + &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content download progress is reported. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnDataProgress( + std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response has been fully handled (either succeeded or failed). + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + virtual void SetOnResponseComplete(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when an error condition is found. + /// + /// + /// The handler's purpose is not to report any given HTTP error status (i.e. 403, 501). + /// It is meant to report application errors when executing HTTP requests. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnError( std::function &&handler) noexcept = 0; }; diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 15d23ec1b5b..cd01c56d7dd 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -120,12 +120,12 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co } /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS white list + // Ensure header is in Simple CORS allowlist for (const auto &header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; @@ -135,12 +135,12 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co // WinRT separates request headers from request content headers if (auto content = request.Content()) { for (const auto &header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + // WinRT automatically appends non-allowlisted header Content-Length when Content-Type is set. Skip it. if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && !boost::iequals(header.Key(), "Content-Length")) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) return false; @@ -148,7 +148,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co } } - // Ensure method is in Simple CORS white list + // Ensure method is in Simple CORS allowlist return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); } @@ -599,7 +599,7 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons } if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist + // Filter out response headers that are not in the Simple CORS allowlist std::queue nonSimpleNames; for (const auto &header : response.Headers().GetView()) { if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) @@ -651,21 +651,26 @@ ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); + auto writeSeparator = false; + for (const auto &header : coRequest.Headers()) { + if (writeSeparator) { + headerNames += L", "; + } else { + writeSeparator = true; + } - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); + headerNames += header.Key(); } if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); + for (const auto &header : coRequest.Content().Headers()) { + if (writeSeparator) { + headerNames += L", "; + } else { + writeSeparator = true; + } - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); + headerNames += header.Key(); } } diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 56e9899531f..6022450205b 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -51,8 +51,51 @@ using winrt::Windows::Web::Http::IHttpClient; using winrt::Windows::Web::Http::IHttpContent; using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; +namespace { + +constexpr uint32_t operator""_KiB(unsigned long long int x) { + return static_cast(1024 * x); +} + +constexpr uint32_t operator""_MiB(unsigned long long int x) { + return static_cast(1024_KiB * x); +} + +constexpr char responseTypeText[] = "text"; +constexpr char responseTypeBase64[] = "base64"; +constexpr char responseTypeBlob[] = "blob"; + +} // namespace namespace Microsoft::React::Networking { +// May throw winrt::hresult_error +void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { + HttpMediaTypeHeaderValue contentType{nullptr}; + + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + // TODO: Consolidate with PerformRequest's header parsing. + for (auto &header : headers.items()) { + auto &name = header.first.getString(); + auto &value = header.second.getString(); + + if (boost::iequals(name.c_str(), "Content-Type")) { + contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + throw hresult_error{E_INVALIDARG, L"Failed to append Authorization"}; + } + } else { + content.Headers().Append(to_hstring(name), to_hstring(value)); + } + } + + if (contentType) { + content.Headers().ContentType(contentType); + } +} + #pragma region WinRTHttpResource WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{std::move(client)} {} @@ -81,20 +124,23 @@ IAsyncOperation WinRTHttpResource::CreateRequest( // Headers are generally case-insensitive // https://www.ietf.org/rfc/rfc2616.txt section 4.2 for (auto &header : reqArgs->Headers) { - if (boost::iequals(header.first.c_str(), "Content-Type")) { - bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(header.second), contentType); + auto &name = header.first; + auto &value = header.second; + + if (boost::iequals(name.c_str(), "Content-Type")) { + bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); if (!success) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); } co_return nullptr; } - } else if (boost::iequals(header.first.c_str(), "Content-Encoding")) { - contentEncoding = header.second; - } else if (boost::iequals(header.first.c_str(), "Content-Length")) { - contentLength = header.second; - } else if (boost::iequals(header.first.c_str(), "Authorization")) { - bool success = request.Headers().TryAppendWithoutValidation(to_hstring(header.first), to_hstring(header.second)); + } else if (boost::iequals(name.c_str(), "Content-Encoding")) { + contentEncoding = value; + } else if (boost::iequals(name.c_str(), "Content-Length")) { + contentLength = value; + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); if (!success) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); @@ -103,7 +149,7 @@ IAsyncOperation WinRTHttpResource::CreateRequest( } } else { try { - request.Headers().Append(to_hstring(header.first), to_hstring(header.second)); + request.Headers().Append(to_hstring(name), to_hstring(value)); } catch (hresult_error const &e) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); @@ -146,9 +192,31 @@ IAsyncOperation WinRTHttpResource::CreateRequest( auto file = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{to_hstring(data["uri"].asString())}); auto stream = co_await file.OpenReadAsync(); content = HttpStreamContent{std::move(stream)}; - } else if (!data["form"].empty()) { - // #9535 - HTTP form data support - // winrt::Windows::Web::Http::HttpMultipartFormDataContent() + } else if (!data["formData"].empty()) { + winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; + auto formData = data["formData"]; + + // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary + contentType = nullptr; + + for (auto &formDataPart : formData) { + IHttpContent formContent{nullptr}; + if (!formDataPart["string"].isNull()) { + formContent = HttpStringContent{to_hstring(formDataPart["string"].asString())}; + } else if (!formDataPart["uri"].empty()) { + auto filePath = to_hstring(formDataPart["uri"].asString()); + auto file = co_await StorageFile::GetFileFromPathAsync(filePath); + auto stream = co_await file.OpenReadAsync(); + formContent = HttpStreamContent{stream}; + } + + if (formContent) { + AttachMultipartHeaders(formContent, formDataPart["headers"]); + multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); + } + } // foreach form data part + + content = multiPartContent; } } @@ -205,7 +273,7 @@ void WinRTHttpResource::SendRequest( bool withCredentials, std::function &&callback) noexcept /*override*/ { // Enforce supported args - assert(responseType == "text" || responseType == "base64" || responseType == "blob"); + assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); if (callback) { callback(requestId); @@ -283,6 +351,22 @@ void WinRTHttpResource::SetOnData(function &&handler) noexcept +/*override*/ { + m_onIncrementalData = std::move(handler); +} + +void WinRTHttpResource::SetOnDataProgress( + function &&handler) noexcept +/*override*/ { + m_onDataProgress = std::move(handler); +} + +void WinRTHttpResource::SetOnResponseComplete(function &&handler) noexcept /*override*/ { + m_onComplete = std::move(handler); +} + void WinRTHttpResource::SetOnError( function &&handler) noexcept /*override*/ { @@ -316,11 +400,18 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect auto props = winrt::multi_threaded_map(); props.Insert(L"RequestArgs", coArgs); - auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props); - if (!coRequest) { - co_return; + auto coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); + co_await lessthrow_await_adapter>{coRequestOp}; + auto coRequestOpHR = coRequestOp.ErrorCode(); + if (coRequestOpHR < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); + } + co_return self->UntrackResponse(reqArgs->RequestId); } + auto coRequest = coRequestOp.GetResults(); + // If URI handler is available, it takes over request processing. if (auto uriHandler = self->m_uriHandler.lock()) { auto uri = winrt::to_string(coRequest.RequestUri().ToString()); @@ -332,6 +423,10 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect self->m_onRequestSuccess(reqArgs->RequestId); } + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } + co_return; } } catch (const hresult_error &e) { @@ -345,6 +440,9 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect try { auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); + + auto isText = reqArgs->ResponseType == responseTypeText; + self->TrackResponse(reqArgs->RequestId, sendRequestOp); if (reqArgs->Timeout > 0) { @@ -411,55 +509,86 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); auto reader = DataReader{inputStream}; - // #9510 - 10mb limit on fetch - co_await reader.LoadAsync(10 * 1024 * 1024); + // Accumulate all incoming request data in 8MB chunks + // Note, the minimum apparent valid chunk size is 128 KB + // Apple's implementation appears to grab 5-8 KB chunks + const uint32_t segmentSize = reqArgs->IncrementalUpdates ? 128_KiB : 8_MiB; // Let response handler take over, if set if (auto responseHandler = self->m_responseHandler.lock()) { if (responseHandler->Supports(reqArgs->ResponseType)) { - auto bytes = vector(reader.UnconsumedBufferLength()); - reader.ReadBytes(bytes); - auto blob = responseHandler->ToResponseData(std::move(bytes)); + vector responseData{}; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + auto data = vector(length); + reader.ReadBytes(data); + + responseData.insert(responseData.cend(), data.cbegin(), data.cend()); + } + + auto blob = responseHandler->ToResponseData(std::move(responseData)); if (self->m_onDataDynamic && self->m_onRequestSuccess) { self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); self->m_onRequestSuccess(reqArgs->RequestId); } + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } co_return; } } - auto isText = reqArgs->ResponseType == "text"; if (isText) { reader.UnicodeEncoding(UnicodeEncoding::Utf8); } - // #9510 - We currently accumulate all incoming request data in 10MB chunks. - uint32_t segmentSize = 10 * 1024 * 1024; + int64_t receivedBytes = 0; string responseData; winrt::Windows::Storage::Streams::IBuffer buffer; - uint32_t length; - do { - co_await reader.LoadAsync(segmentSize); - length = reader.UnconsumedBufferLength(); + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + receivedBytes += length; if (isText) { - auto data = std::vector(length); + auto data = vector(length); reader.ReadBytes(data); - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + auto incrementData = string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + // #9534 - Send incremental updates. + // See https://github.com/facebook/react-native/blob/v0.70.6/Libraries/Network/RCTNetworking.mm#L561 + if (reqArgs->IncrementalUpdates) { + responseData = std::move(incrementData); + + if (self->m_onIncrementalData) { + // For total, see #10849 + self->m_onIncrementalData(reqArgs->RequestId, std::move(responseData), receivedBytes, 0 /*total*/); + } + } else { + responseData += std::move(incrementData); + } } else { buffer = reader.ReadBuffer(length); auto data = CryptographicBuffer::EncodeToBase64String(buffer); responseData += winrt::to_string(std::wstring_view(data)); + + if (self->m_onDataProgress) { + // For total, see #10849 + self->m_onDataProgress(reqArgs->RequestId, receivedBytes, 0 /*total*/); + } } - } while (length > 0); + } - if (self->m_onData) { + // If dealing with text-incremental response data, use m_onIncrementalData instead + if (self->m_onData && !(reqArgs->IncrementalUpdates && isText)) { self->m_onData(reqArgs->RequestId, std::move(responseData)); } + + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } } else { if (self->m_onError) { self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 23cc68dd299..7b18c69c6aa 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -30,6 +30,10 @@ class WinRTHttpResource : public IHttpResource, std::function m_onData; std::function m_onDataDynamic; std::function m_onError; + std::function + m_onIncrementalData; + std::function m_onDataProgress; + std::function m_onComplete; // Used for IHttpModuleProxy std::weak_ptr m_uriHandler; @@ -80,6 +84,12 @@ class WinRTHttpResource : public IHttpResource, void SetOnResponse(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; + void SetOnIncrementalData( + std::function + &&handler) noexcept override; + void SetOnDataProgress( + std::function &&handler) noexcept override; + void SetOnResponseComplete(std::function &&handler) noexcept override; void SetOnError( std::function &&handler) noexcept override; diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 59fa2e23a8f..ecf7874d0ca 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -27,7 +27,6 @@ #include #include -#include #include #include #include @@ -73,11 +72,7 @@ namespace Microsoft::React { /*extern*/ std::unique_ptr CreateHttpModule( winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { - if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { - return std::make_unique(); - } else { - return std::make_unique(inspectableProperties); - } + return std::make_unique(inspectableProperties); } } // namespace Microsoft::React @@ -638,8 +633,7 @@ std::vector> InstanceImpl::GetDefaultNativeModules // If this code is enabled, we will have unused module instances. // Also, MSRN has a different property bag mechanism incompatible with this method's transitionalProps variable. #if (defined(_MSC_VER) && !defined(WINRT)) - if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule") && - !Microsoft::React::GetRuntimeOptionBool("Http.UseMonolithicModule")) { + if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule")) { modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetBlobModuleName(), diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index b08b35b58ea..ebca7b446c0 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -50,7 +50,9 @@ - + + true + diff --git a/vnext/src/IntegrationTests/BlobTest.js b/vnext/src/IntegrationTests/BlobTest.js index a9a87a365d5..4070b8f8176 100644 --- a/vnext/src/IntegrationTests/BlobTest.js +++ b/vnext/src/IntegrationTests/BlobTest.js @@ -16,7 +16,7 @@ const {TestModule} = ReactNative.NativeModules; type State = { statusCode: number, xhr: XMLHttpRequest, - expected: String, + expected: string, }; class BlobTest extends React.Component<{...}, State> { @@ -24,123 +24,122 @@ class BlobTest extends React.Component<{...}, State> { statusCode: 0, xhr: new XMLHttpRequest(), // https://www.facebook.com/favicon.ico - expected: new String( + expected: 'data:application/octet-stream;base64,' + - 'AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAA' + - 'AAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFl' + - 'BiviZgKP4WYB1f//////////4WUA1eJmAI/hawYrAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAA/4ArBuNpA5TkawP942kC/+NpAv///////////+NpAv/jaQL/5GoD/eNp' + - 'A5T/gCsGAAAAAAAAAAAAAAAA/4ArBuVvBL3lbgT/5W4E/+VuBP/lbgT/////////' + - '///lbgT/5W4E/+VuBP/lbgT/5W8Evf+AKwYAAAAAAAAAAOlzBZTncwX/53MF/+dz' + - 'Bf/ncwX/53MF////////////53MF/+dzBf/ncwb/53MF/+dzBv/pcweUAAAAAO19' + - 'DCvpeAf96HcH/+l4B//odwf/6XgH/+l4B////////////+h3B//odwf/6XgH/+h3' + - 'B//peAf/6nkH/e19DCvrfQmP630J/+t9Cf/rfAn/630J/+t8Cf/rfAn/////////' + - '///rfQn/630J/+p8CP/rfQn/6nwI/+p8CP/rfQuP7YEL1e2BCv/tgQr/7IEK/+2B' + - 'Cv/////////////////////////////////uiRj/7IEK/+2CCv/tggr/7YIM1e6G' + - 'DfPvhgz/74YM/++HDP/vhgz/////////////////////////////////8Zw4/++G' + - 'DP/uhgz/7oYM/+6GDPPwiw7z8IsN//CLDf/wiw3/8IsN//CLDf/wiw3/////////' + - '///wig3/8IoN//CLDf/wig3/8IsN//CLDf/xjA/z85EQ1fOQD//zkA//85AP//OQ' + - 'D//zkA//85AP////////////8o8P//KPD//ykA//8o8P//KQD//ykA//85EQ1fSU' + - 'EI/1lRH/9ZUR//SUEP/1lRH/9JQQ//SUEP/+9uz///////jDev/0mRz/9ZUR//SV' + - 'Ef/1lRH/9ZUR//SUEI/5mhgr95kS/faZEv/2mRL/9pkS//aZEv/2mRL//Nqo////' + - '//////////////rLhv/3mhL/9pkS//eZEv35mhgrAAAAAPifFZT4nhT/+Z8U//ie' + - 'FP/5nxT/+Z8U//ikI//83a3//vjw//78+f/7yX3/+J4T//ieFP/4nxWUAAAAAAAA' + - 'AAD/qisG+6MWvfqjFf/6oxX/+qMV//qjFf/6oxX/+qMV//qjFf/6oxX/+qIV//qj' + - 'Ff/7oxa9/6orBgAAAAAAAAAAAAAAAP+qKwb9qRiU/agW/fyoF//8qBf//agX//yo' + - 'F//9qBf//agX//2oF/39qRiU/6orBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+y' + - 'Hiv/rRmP/6wZ1f+tGPP/rBjz/64Z1f+vGY//sh4rAAAAAAAAAAAAAAAAAAAAAPAP' + - 'AADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB' + - 'AACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+A' + - 'AAbiZQRH4GMAlf//////////////////////////4GQAv+BjAJXiZQBH/4AABgAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpqCxjiZgKW4WYB8eJmAf/hZQH/////////' + - '///////////////////hZgH/4mYB/+FmAf/iZwHx4mYClupqCxgAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9t' + - 'JAfkagSC42kC9ONoAv/jaAL/4mgC/+NoAv///////////////////////////+Jo' + - 'Af/iaAL/4mgB/+JoAv/iaAL/42kC9ORqBIL/bSQHAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqagsY5GoEx+NqA//jagP/42oD/+Nq' + - 'A//kawP/42oD////////////////////////////42oD/+RrA//jagP/5GsD/+Rr' + - 'A//kawP/5GsD/+RsBMfqdQsYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAA6HEGLeVuBOPlbQT/5GwD/+VtBP/kbAP/5GwD/+VtBP/kbAP/////////' + - '///////////////////kbAP/5G0D/+RsA//kbQP/5G0D/+RtA//kbQP/5G0D/+Ru' + - 'A+PocQYtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOp1CxjmcAbj5W8E/+Vv' + - 'BP/lbwT/5W8E/+VvBP/lbwT/5W8E/+VvBP///////////////////////////+Vv' + - 'BP/mcAX/5W8E/+ZwBf/mcAX/5W8E/+ZwBf/lbwT/5W8E/+ZwBuPqdQsYAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAD/kiQH53IFx+ZyBf/mcQX/5nEF/+ZxBf/mcQX/5nEF/+Zx' + - 'Bf/ncgX/5nEF////////////////////////////53IG/+ZxBf/ncgb/5nEF/+Zx' + - 'Bf/mcgX/5nEF/+ZyBf/mcgX/5nEF/+dyBcf/kiQHAAAAAAAAAAAAAAAAAAAAAOd2' + - 'CILodAb/53QG/+h0Bv/odAb/6HQG/+h0Bv/odAb/6HQG/+d0Bv/odAb/////////' + - '///////////////////ndAX/53QG/+d0Bf/ndAb/53QG/+h1Bv/ndAb/6HUG/+h1' + - 'Bv/ndAX/6HUG/+d2BoIAAAAAAAAAAAAAAADqgAsY6HYG9Oh2B//odgb/6HYH/+h2' + - 'B//odgb/6HYH/+h2Bv/odgb/6HYH/+h2Bv///////////////////////////+l3' + - 'B//odgb/6XcH/+h2Bv/odgb/6HYH/+h2Bv/odgf/6HYH/+h2Bv/odgf/6HYG9OqA' + - 'CxgAAAAAAAAAAOt6CZbpeQj/6nkI/+l5CP/qeQj/6nkI/+l5B//qeQj/6XkH/+l5' + - 'B//peQf/6XkH////////////////////////////6XkI/+l5B//peQj/6XkH/+l5' + - 'B//peQj/6XkH/+l5CP/peQj/6XkH/+l5CP/peQf/63oJlgAAAAD/gCsG7H0K8et8' + - 'Cf/qewj/63wJ/+p7CP/qewj/6nsI/+p7CP/qewj/6nsI/+t8Cf/qewj/////////' + - '///////////////////qewj/6nwJ/+p7CP/qfAn/6nwJ/+p7CP/qfAn/6nsI/+p7' + - 'CP/rfAn/6nsI/+t8Cf/sfQrx/4ArBu2BC0frfQn/630J/+t+Cf/rfQn/634J/+t+' + - 'Cf/rfgn/634J////////////////////////////////////////////////////' + - '///////////////////zs27/634J/+x+Cf/rfgn/634J/+t+Cf/rfgn/634J/+t+' + - 'Cf/tgQtH7IAKleyACv/sgAr/7IAK/+yACv/sgAr/7IAK/+yACv/sgAr/////////' + - '//////////////////////////////////////////////////////////////XC' + - 'iv/sgAr/7IAK/+yACv/sgAr/7IAJ/+yACv/sgAn/7IAJ/+yACpXugwu/7YML/+2D' + - 'C//tggr/7YML/+2CCv/tggr/7YIK/+2CCv//////////////////////////////' + - '////////////////////////////////////////+NKn/+2DC//tggr/7YML/+2D' + - 'C//tgwv/7YML/+2DC//tgwv/7oMLv++GDNnuhQv/7oUL/+6FC//uhQv/7oUL/+6F' + - 'C//vhQz/7oUL////////////////////////////////////////////////////' + - '///////////////////64cT/7oUL/+6FC//uhQv/7oUL/+6EC//uhQv/7oQL/+6E' + - 'C//vhgzZ74gO8++IDP/viAz/74cM/++IDP/vhwz/74cM/++HDP/vhwz/////////' + - '//////////////////////////////////////////////////////////////3w' + - '4f/viA3/74cM/++IDf/viA3/74cM/++IDf/vhwz/74cM/++HDfPwiw7z8IoN//CK' + - 'Df/wig3/8IoN//CKDf/wig3/8IkN//CKDf/wiQ3/8IkN//CKDf/wiQ3/////////' + - '///////////////////wiQ3/8IoN//CJDf/wig3/8IoN//CJDf/wig3/8IkN//CJ' + - 'Df/wiQ3/8IkN//CJDf/wiQ3/8IsO8/KNDtnxjA7/8YwO//GMDf/xjA7/8YwN//GM' + - 'Df/xjA3/8YwN//GMDf/xjA3/8YwO//GMDf////////////////////////////GM' + - 'Dv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GM' + - 'Dv/yjQ7Z8o8Pv/KPD//yjw//8o8P//KPD//yjw//8o8P//KPD//yjw//8o8P//KP' + - 'D//yjg7/8o8P////////////////////////////8Y4O//KODv/xjg7/8o4O//KO' + - 'Dv/yjg7/8o4O//KODv/yjg7/8o8P//KODv/yjw//8o8P//OQEL/zkQ+V85EP//OR' + - 'D//zkQ//85EP//ORD//zkQ//85EP//ORD//zkQ//85EP//OREP/zkQ///vr0////' + - '///////////////////0myb/85EQ//ORD//zkRD/85EQ//ORD//zkRD/85EP//OR' + - 'D//zkQ//85EP//ORD//zkQ//85EPlfSXEkf0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + - 'EP/zkxD/9JQQ//OTEP/zkxD/9JQQ//OTEP/86tD///////////////////////rV' + - 'ov/1nSb/85MQ//STEP/0kxD/9JMQ//STEP/0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + - 'EP/0kxJH/6orBvWWEvH1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYR//WW' + - 'Ef/1lhH/9ZYR//vZq///////////////////////////////////////////////' + - '///1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYS8f+qKwYAAAAA95kTlvaY' + - 'Ev/2mBH/9pgS//aYEf/2mBH/9ZgR//aYEf/1mBH/9ZgR//aYEv/1mBH/+LFN////' + - '//////////////////////////////////////////////aYEv/1mBH/9pgS//aY' + - 'Ev/1mBH/9pgS//WYEf/3mRGWAAAAAAAAAAD/nxUY+JwU9PebE//3mxP/95sT//eb' + - 'E//3mxP/95sT//ebE//3mxP/95oS//ebE//3mhL//OK7////////////////////' + - '////////////////////////95sT//ebE//3mxP/95sT//aaEv/3mxP/95sT9P+f' + - 'FRgAAAAAAAAAAAAAAAD5nxSC+J0T//idE//4nRP/+J0T//ecE//4nRP/95wT//ec' + - 'E//4nRP/95wT//idE//4pSf//efF////////////////////////////////////' + - '///4nRP/950T//idE//4nRP/+J0T//idE//5nxSCAAAAAAAAAAAAAAAAAAAAAP+2' + - 'JAf6oBXH+aAU//mgFP/5oBT/+J8U//mgFP/4nxT/+J8U//mfFP/4nxT/+Z8U//mf' + - 'FP/5oRf/+86H//7w2v/+/Pj//v36//758f/+8+P//evQ//mgFP/5nxT/+aAU//mg' + - 'FP/4nxT/+qAVx/+2JAcAAAAAAAAAAAAAAAAAAAAAAAAAAP+qFRj7oxXj+aEU//mh' + - 'FP/6ohX/+aEU//qiFf/6ohX/+qIV//qiFf/6ohX/+qIV//mhFP/6ohX/+aEU//mh' + - 'FP/6ohX/+aEU//qiFf/6ohX/+aEU//qiFf/5oRT/+aEU//ujFeP/qhUYAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+mFi78pBXj+6QV//ukFv/7pBX/+6QW//uk' + - 'Fv/6pBX/+6QW//qkFf/6pBX/+6QW//qkFf/7pBb/+6QW//ulFv/7pBb/+6UW//ul' + - 'Fv/7pBX/+6UW//ukFf/8pBXj/6QXLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAP+qIBj8qBfH/KcW//ynF//8pxb//KcW//ynFv/8pxb//KcW//yn' + - 'Fv/7phb//KcW//umFv/7phb/+6YW//umFv/7phb/+6YW//ynFv/7phb//KgXx/+q' + - 'IBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+2' + - 'JAf9qxiC/akY9PypF//8qRf//KgX//ypF//8qBf//KgX//2pF//8qBf//akX//2p' + - 'F//9qRf//akX//2pF//9qRf//qkY9P2rGIL/tiQHAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/tSAY/60alv+s' + - 'GPH+rBj//qwY//6sGP/+rBj//asY//6sGP/9qxj//asY//2rF//9qxj//qsX8f2s' + - 'GJb/tSAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9UrBv+wGUf/rxqV/68Zv/+v' + - 'Gtn/rhnz/64Z8/+vGtn/rxm//68alf+wGUf/1SsGAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AA' + - 'AAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8A' + - 'AP//wAP/', - ), + 'AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAA' + + 'AAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFl' + + 'BiviZgKP4WYB1f//////////4WUA1eJmAI/hawYrAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA/4ArBuNpA5TkawP942kC/+NpAv///////////+NpAv/jaQL/5GoD/eNp' + + 'A5T/gCsGAAAAAAAAAAAAAAAA/4ArBuVvBL3lbgT/5W4E/+VuBP/lbgT/////////' + + '///lbgT/5W4E/+VuBP/lbgT/5W8Evf+AKwYAAAAAAAAAAOlzBZTncwX/53MF/+dz' + + 'Bf/ncwX/53MF////////////53MF/+dzBf/ncwb/53MF/+dzBv/pcweUAAAAAO19' + + 'DCvpeAf96HcH/+l4B//odwf/6XgH/+l4B////////////+h3B//odwf/6XgH/+h3' + + 'B//peAf/6nkH/e19DCvrfQmP630J/+t9Cf/rfAn/630J/+t8Cf/rfAn/////////' + + '///rfQn/630J/+p8CP/rfQn/6nwI/+p8CP/rfQuP7YEL1e2BCv/tgQr/7IEK/+2B' + + 'Cv/////////////////////////////////uiRj/7IEK/+2CCv/tggr/7YIM1e6G' + + 'DfPvhgz/74YM/++HDP/vhgz/////////////////////////////////8Zw4/++G' + + 'DP/uhgz/7oYM/+6GDPPwiw7z8IsN//CLDf/wiw3/8IsN//CLDf/wiw3/////////' + + '///wig3/8IoN//CLDf/wig3/8IsN//CLDf/xjA/z85EQ1fOQD//zkA//85AP//OQ' + + 'D//zkA//85AP////////////8o8P//KPD//ykA//8o8P//KQD//ykA//85EQ1fSU' + + 'EI/1lRH/9ZUR//SUEP/1lRH/9JQQ//SUEP/+9uz///////jDev/0mRz/9ZUR//SV' + + 'Ef/1lRH/9ZUR//SUEI/5mhgr95kS/faZEv/2mRL/9pkS//aZEv/2mRL//Nqo////' + + '//////////////rLhv/3mhL/9pkS//eZEv35mhgrAAAAAPifFZT4nhT/+Z8U//ie' + + 'FP/5nxT/+Z8U//ikI//83a3//vjw//78+f/7yX3/+J4T//ieFP/4nxWUAAAAAAAA' + + 'AAD/qisG+6MWvfqjFf/6oxX/+qMV//qjFf/6oxX/+qMV//qjFf/6oxX/+qIV//qj' + + 'Ff/7oxa9/6orBgAAAAAAAAAAAAAAAP+qKwb9qRiU/agW/fyoF//8qBf//agX//yo' + + 'F//9qBf//agX//2oF/39qRiU/6orBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+y' + + 'Hiv/rRmP/6wZ1f+tGPP/rBjz/64Z1f+vGY//sh4rAAAAAAAAAAAAAAAAAAAAAPAP' + + 'AADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB' + + 'AACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+A' + + 'AAbiZQRH4GMAlf//////////////////////////4GQAv+BjAJXiZQBH/4AABgAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpqCxjiZgKW4WYB8eJmAf/hZQH/////////' + + '///////////////////hZgH/4mYB/+FmAf/iZwHx4mYClupqCxgAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9t' + + 'JAfkagSC42kC9ONoAv/jaAL/4mgC/+NoAv///////////////////////////+Jo' + + 'Af/iaAL/4mgB/+JoAv/iaAL/42kC9ORqBIL/bSQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqagsY5GoEx+NqA//jagP/42oD/+Nq' + + 'A//kawP/42oD////////////////////////////42oD/+RrA//jagP/5GsD/+Rr' + + 'A//kawP/5GsD/+RsBMfqdQsYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA6HEGLeVuBOPlbQT/5GwD/+VtBP/kbAP/5GwD/+VtBP/kbAP/////////' + + '///////////////////kbAP/5G0D/+RsA//kbQP/5G0D/+RtA//kbQP/5G0D/+Ru' + + 'A+PocQYtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOp1CxjmcAbj5W8E/+Vv' + + 'BP/lbwT/5W8E/+VvBP/lbwT/5W8E/+VvBP///////////////////////////+Vv' + + 'BP/mcAX/5W8E/+ZwBf/mcAX/5W8E/+ZwBf/lbwT/5W8E/+ZwBuPqdQsYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAD/kiQH53IFx+ZyBf/mcQX/5nEF/+ZxBf/mcQX/5nEF/+Zx' + + 'Bf/ncgX/5nEF////////////////////////////53IG/+ZxBf/ncgb/5nEF/+Zx' + + 'Bf/mcgX/5nEF/+ZyBf/mcgX/5nEF/+dyBcf/kiQHAAAAAAAAAAAAAAAAAAAAAOd2' + + 'CILodAb/53QG/+h0Bv/odAb/6HQG/+h0Bv/odAb/6HQG/+d0Bv/odAb/////////' + + '///////////////////ndAX/53QG/+d0Bf/ndAb/53QG/+h1Bv/ndAb/6HUG/+h1' + + 'Bv/ndAX/6HUG/+d2BoIAAAAAAAAAAAAAAADqgAsY6HYG9Oh2B//odgb/6HYH/+h2' + + 'B//odgb/6HYH/+h2Bv/odgb/6HYH/+h2Bv///////////////////////////+l3' + + 'B//odgb/6XcH/+h2Bv/odgb/6HYH/+h2Bv/odgf/6HYH/+h2Bv/odgf/6HYG9OqA' + + 'CxgAAAAAAAAAAOt6CZbpeQj/6nkI/+l5CP/qeQj/6nkI/+l5B//qeQj/6XkH/+l5' + + 'B//peQf/6XkH////////////////////////////6XkI/+l5B//peQj/6XkH/+l5' + + 'B//peQj/6XkH/+l5CP/peQj/6XkH/+l5CP/peQf/63oJlgAAAAD/gCsG7H0K8et8' + + 'Cf/qewj/63wJ/+p7CP/qewj/6nsI/+p7CP/qewj/6nsI/+t8Cf/qewj/////////' + + '///////////////////qewj/6nwJ/+p7CP/qfAn/6nwJ/+p7CP/qfAn/6nsI/+p7' + + 'CP/rfAn/6nsI/+t8Cf/sfQrx/4ArBu2BC0frfQn/630J/+t+Cf/rfQn/634J/+t+' + + 'Cf/rfgn/634J////////////////////////////////////////////////////' + + '///////////////////zs27/634J/+x+Cf/rfgn/634J/+t+Cf/rfgn/634J/+t+' + + 'Cf/tgQtH7IAKleyACv/sgAr/7IAK/+yACv/sgAr/7IAK/+yACv/sgAr/////////' + + '//////////////////////////////////////////////////////////////XC' + + 'iv/sgAr/7IAK/+yACv/sgAr/7IAJ/+yACv/sgAn/7IAJ/+yACpXugwu/7YML/+2D' + + 'C//tggr/7YML/+2CCv/tggr/7YIK/+2CCv//////////////////////////////' + + '////////////////////////////////////////+NKn/+2DC//tggr/7YML/+2D' + + 'C//tgwv/7YML/+2DC//tgwv/7oMLv++GDNnuhQv/7oUL/+6FC//uhQv/7oUL/+6F' + + 'C//vhQz/7oUL////////////////////////////////////////////////////' + + '///////////////////64cT/7oUL/+6FC//uhQv/7oUL/+6EC//uhQv/7oQL/+6E' + + 'C//vhgzZ74gO8++IDP/viAz/74cM/++IDP/vhwz/74cM/++HDP/vhwz/////////' + + '//////////////////////////////////////////////////////////////3w' + + '4f/viA3/74cM/++IDf/viA3/74cM/++IDf/vhwz/74cM/++HDfPwiw7z8IoN//CK' + + 'Df/wig3/8IoN//CKDf/wig3/8IkN//CKDf/wiQ3/8IkN//CKDf/wiQ3/////////' + + '///////////////////wiQ3/8IoN//CJDf/wig3/8IoN//CJDf/wig3/8IkN//CJ' + + 'Df/wiQ3/8IkN//CJDf/wiQ3/8IsO8/KNDtnxjA7/8YwO//GMDf/xjA7/8YwN//GM' + + 'Df/xjA3/8YwN//GMDf/xjA3/8YwO//GMDf////////////////////////////GM' + + 'Dv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GM' + + 'Dv/yjQ7Z8o8Pv/KPD//yjw//8o8P//KPD//yjw//8o8P//KPD//yjw//8o8P//KP' + + 'D//yjg7/8o8P////////////////////////////8Y4O//KODv/xjg7/8o4O//KO' + + 'Dv/yjg7/8o4O//KODv/yjg7/8o8P//KODv/yjw//8o8P//OQEL/zkQ+V85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EP//ORD//zkQ//85EP//OREP/zkQ///vr0////' + + '///////////////////0myb/85EQ//ORD//zkRD/85EQ//ORD//zkRD/85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EPlfSXEkf0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/zkxD/9JQQ//OTEP/zkxD/9JQQ//OTEP/86tD///////////////////////rV' + + 'ov/1nSb/85MQ//STEP/0kxD/9JMQ//STEP/0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/0kxJH/6orBvWWEvH1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYR//WW' + + 'Ef/1lhH/9ZYR//vZq///////////////////////////////////////////////' + + '///1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYS8f+qKwYAAAAA95kTlvaY' + + 'Ev/2mBH/9pgS//aYEf/2mBH/9ZgR//aYEf/1mBH/9ZgR//aYEv/1mBH/+LFN////' + + '//////////////////////////////////////////////aYEv/1mBH/9pgS//aY' + + 'Ev/1mBH/9pgS//WYEf/3mRGWAAAAAAAAAAD/nxUY+JwU9PebE//3mxP/95sT//eb' + + 'E//3mxP/95sT//ebE//3mxP/95oS//ebE//3mhL//OK7////////////////////' + + '////////////////////////95sT//ebE//3mxP/95sT//aaEv/3mxP/95sT9P+f' + + 'FRgAAAAAAAAAAAAAAAD5nxSC+J0T//idE//4nRP/+J0T//ecE//4nRP/95wT//ec' + + 'E//4nRP/95wT//idE//4pSf//efF////////////////////////////////////' + + '///4nRP/950T//idE//4nRP/+J0T//idE//5nxSCAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf6oBXH+aAU//mgFP/5oBT/+J8U//mgFP/4nxT/+J8U//mfFP/4nxT/+Z8U//mf' + + 'FP/5oRf/+86H//7w2v/+/Pj//v36//758f/+8+P//evQ//mgFP/5nxT/+aAU//mg' + + 'FP/4nxT/+qAVx/+2JAcAAAAAAAAAAAAAAAAAAAAAAAAAAP+qFRj7oxXj+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+qIV//qiFf/6ohX/+qIV//mhFP/6ohX/+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+aEU//qiFf/5oRT/+aEU//ujFeP/qhUYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+mFi78pBXj+6QV//ukFv/7pBX/+6QW//uk' + + 'Fv/6pBX/+6QW//qkFf/6pBX/+6QW//qkFf/7pBb/+6QW//ulFv/7pBb/+6UW//ul' + + 'Fv/7pBX/+6UW//ukFf/8pBXj/6QXLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAP+qIBj8qBfH/KcW//ynF//8pxb//KcW//ynFv/8pxb//KcW//yn' + + 'Fv/7phb//KcW//umFv/7phb/+6YW//umFv/7phb/+6YW//ynFv/7phb//KgXx/+q' + + 'IBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf9qxiC/akY9PypF//8qRf//KgX//ypF//8qBf//KgX//2pF//8qBf//akX//2p' + + 'F//9qRf//akX//2pF//9qRf//qkY9P2rGIL/tiQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/tSAY/60alv+s' + + 'GPH+rBj//qwY//6sGP/+rBj//asY//6sGP/9qxj//asY//2rF//9qxj//qsX8f2s' + + 'GJb/tSAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9UrBv+wGUf/rxqV/68Zv/+v' + + 'Gtn/rhnz/64Z8/+vGtn/rxm//68alf+wGUf/1SsGAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AA' + + 'AAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8A' + + 'AP//wAP/', }; _get = () => {