UNPKG

matrix-react-sdk

Version:
582 lines (567 loc) 101 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _matrix = require("matrix-js-sdk/src/matrix"); var _compoundWeb = require("@vector-im/compound-web"); var HtmlUtils = _interopRequireWildcard(require("../../../HtmlUtils")); var _DateUtils = require("../../../DateUtils"); var _Modal = _interopRequireDefault(require("../../../Modal")); var _dispatcher = _interopRequireDefault(require("../../../dispatcher/dispatcher")); var _languageHandler = require("../../../languageHandler"); var _ContextMenu = _interopRequireWildcard(require("../../structures/ContextMenu")); var ContextMenu = _ContextMenu; var _SettingsStore = _interopRequireDefault(require("../../../settings/SettingsStore")); var _pillify = require("../../../utils/pillify"); var _tooltipify = require("../../../utils/tooltipify"); var _IntegrationManagers = require("../../../integrations/IntegrationManagers"); var _Permalinks = require("../../../utils/permalinks/Permalinks"); var _strings = require("../../../utils/strings"); var _UIStore = _interopRequireDefault(require("../../../stores/UIStore")); var _actions = require("../../../dispatcher/actions"); var _GenericTextContextMenu = _interopRequireDefault(require("../context_menus/GenericTextContextMenu")); var _Spoiler = _interopRequireDefault(require("../elements/Spoiler")); var _QuestionDialog = _interopRequireDefault(require("../dialogs/QuestionDialog")); var _MessageEditHistoryDialog = _interopRequireDefault(require("../dialogs/MessageEditHistoryDialog")); var _EditMessageComposer = _interopRequireDefault(require("../rooms/EditMessageComposer")); var _LinkPreviewGroup = _interopRequireDefault(require("../rooms/LinkPreviewGroup")); var _RoomContext = _interopRequireDefault(require("../../../contexts/RoomContext")); var _AccessibleButton = _interopRequireDefault(require("../elements/AccessibleButton")); var _linkifyMatrix = require("../../../linkify-matrix"); var _Reply = require("../../../utils/Reply"); var _wysiwyg_composer = require("../rooms/wysiwyg_composer"); var _MatrixClientPeg = require("../../../MatrixClientPeg"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /* Copyright 2024 New Vector Ltd. Copyright 2015-2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ const MAX_HIGHLIGHT_LENGTH = 4096; class TextualBody extends _react.default.Component { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "contentRef", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "unmounted", false); (0, _defineProperty2.default)(this, "pills", []); (0, _defineProperty2.default)(this, "tooltips", []); (0, _defineProperty2.default)(this, "state", { links: [], widgetHidden: false }); (0, _defineProperty2.default)(this, "onCancelClick", () => { this.setState({ widgetHidden: true }); // FIXME: persist this somewhere smarter than local storage if (global.localStorage) { global.localStorage.setItem("hide_preview_" + this.props.mxEvent.getId(), "1"); } this.forceUpdate(); }); (0, _defineProperty2.default)(this, "onEmoteSenderClick", () => { const mxEvent = this.props.mxEvent; _dispatcher.default.dispatch({ action: _actions.Action.ComposerInsert, userId: mxEvent.getSender(), timelineRenderingType: this.context.timelineRenderingType }); }); /** * This acts as a fallback in-app navigation handler for any body links that * were ignored as part of linkification because they were already links * to start with (e.g. pills, links in the content). */ (0, _defineProperty2.default)(this, "onBodyLinkClick", e => { let target = e.target; // links processed by linkifyjs have their own handler so don't handle those here if (target.classList.contains(_linkifyMatrix.options.className)) return; if (target.nodeName !== "A") { // Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section target = target.closest("a"); } if (!target) return; const localHref = (0, _Permalinks.tryTransformPermalinkToLocalHref)(target.href); if (localHref !== target.href) { // it could be converted to a localHref -> therefore handle locally e.preventDefault(); window.location.hash = localHref; } }); (0, _defineProperty2.default)(this, "getEventTileOps", () => ({ isWidgetHidden: () => { return this.state.widgetHidden; }, unhideWidget: () => { this.setState({ widgetHidden: false }); if (global.localStorage) { global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId()); } } })); (0, _defineProperty2.default)(this, "onStarterLinkClick", (starterLink, ev) => { ev.preventDefault(); // We need to add on our scalar token to the starter link, but we may not have one! // In addition, we can't fetch one on click and then go to it immediately as that // is then treated as a popup! // We can get around this by fetching one now and showing a "confirmation dialog" (hurr hurr) // which requires the user to click through and THEN we can open the link in a new tab because // the window.open command occurs in the same stack frame as the onClick callback. const managers = _IntegrationManagers.IntegrationManagers.sharedInstance(); if (!managers.hasManager()) { managers.openNoManagerDialog(); return; } // Go fetch a scalar token const integrationManager = managers.getPrimaryManager(); const scalarClient = integrationManager?.getScalarClient(); scalarClient?.connect().then(() => { const completeUrl = scalarClient.getStarterLink(starterLink); const integrationsUrl = integrationManager.uiUrl; _Modal.default.createDialog(_QuestionDialog.default, { title: (0, _languageHandler._t)("timeline|scalar_starter_link|dialog_title"), description: /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("timeline|scalar_starter_link|dialog_description", { integrationsUrl: integrationsUrl })), button: (0, _languageHandler._t)("action|continue"), onFinished(confirmed) { if (!confirmed) { return; } const width = window.screen.width > 1024 ? 1024 : window.screen.width; const height = window.screen.height > 800 ? 800 : window.screen.height; const left = (window.screen.width - width) / 2; const top = (window.screen.height - height) / 2; const features = `height=${height}, width=${width}, top=${top}, left=${left},`; const wnd = window.open(completeUrl, "_blank", features); wnd.opener = null; } }); }); }); (0, _defineProperty2.default)(this, "openHistoryDialog", async () => { _Modal.default.createDialog(_MessageEditHistoryDialog.default, { mxEvent: this.props.mxEvent }); }); } componentDidMount() { if (!this.props.editState) { this.applyFormatting(); } } applyFormatting() { // Function is only called from render / componentDidMount → contentRef is set const content = this.contentRef.current; const showLineNumbers = _SettingsStore.default.getValue("showCodeLineNumbers"); this.activateSpoilers([content]); HtmlUtils.linkifyElement(content); (0, _pillify.pillifyLinks)(_MatrixClientPeg.MatrixClientPeg.safeGet(), [content], this.props.mxEvent, this.pills); this.calculateUrlPreview(); // tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip // container is empty before the internal component has mounted so calculateUrlPreview // won't find any anchors (0, _tooltipify.tooltipifyLinks)([content], this.pills, this.tooltips); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons const pres = _reactDom.default.findDOMNode(this).getElementsByTagName("pre"); if (pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. // This happens after the codeblock was edited. if (pres[i].parentElement?.className == "mx_EventTile_pre_container") continue; // Add code element if it's missing since we depend on it if (pres[i].getElementsByTagName("code").length == 0) { this.addCodeElement(pres[i]); } // Wrap a div around <pre> so that the copy button can be correctly positioned // when the <pre> overflows and is scrolled horizontally. const div = this.wrapInDiv(pres[i]); this.handleCodeBlockExpansion(pres[i]); this.addCodeExpansionButton(div, pres[i]); this.addCodeCopyButton(div); if (showLineNumbers) { this.addLineNumbers(pres[i]); } } } // Highlight code const codes = _reactDom.default.findDOMNode(this).getElementsByTagName("code"); if (codes.length > 0) { // Do this asynchronously: parsing code takes time and we don't // need to block the DOM update on it. window.setTimeout(() => { if (this.unmounted) return; for (let i = 0; i < codes.length; i++) { this.highlightCode(codes[i]); } }, 10); } } } addCodeElement(pre) { const code = document.createElement("code"); code.append(...pre.childNodes); pre.appendChild(code); } addCodeExpansionButton(div, pre) { // Calculate how many percent does the pre element take up. // If it's less than 30% we don't add the expansion button. // We also round the number as it sometimes can be 29.99... const percentageOfViewport = Math.round(pre.offsetHeight / _UIStore.default.instance.windowHeight * 100); // TODO: additionally show the button if it's an expanded quoted message if (percentageOfViewport < 30) return; const button = document.createElement("span"); button.className = "mx_EventTile_button "; if (pre.className == "mx_EventTile_collapsedCodeBlock") { button.className += "mx_EventTile_expandButton"; } else { button.className += "mx_EventTile_collapseButton"; } button.onclick = async () => { button.className = "mx_EventTile_button "; if (pre.className == "mx_EventTile_collapsedCodeBlock") { pre.className = ""; button.className += "mx_EventTile_collapseButton"; } else { pre.className = "mx_EventTile_collapsedCodeBlock"; button.className += "mx_EventTile_expandButton"; } // By expanding/collapsing we changed // the height, therefore we call this this.props.onHeightChanged?.(); }; div.appendChild(button); } addCodeCopyButton(div) { const button = document.createElement("span"); button.className = "mx_EventTile_button mx_EventTile_copyButton "; // Check if expansion button exists. If so we put the copy button to the bottom const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button"); if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom"; button.onclick = async () => { const copyCode = button.parentElement?.getElementsByTagName("code")[0]; const successful = copyCode?.textContent ? await (0, _strings.copyPlaintext)(copyCode.textContent) : false; const buttonRect = button.getBoundingClientRect(); const { close } = ContextMenu.createMenu(_GenericTextContextMenu.default, _objectSpread(_objectSpread({}, (0, _ContextMenu.toRightOf)(buttonRect, 0)), {}, { chevronFace: _ContextMenu.ChevronFace.None, message: successful ? (0, _languageHandler._t)("common|copied") : (0, _languageHandler._t)("error|failed_copy") })); button.onmouseleave = close; }; div.appendChild(button); } wrapInDiv(pre) { const div = document.createElement("div"); div.className = "mx_EventTile_pre_container"; // Insert containing div in place of <pre> block pre.parentNode?.replaceChild(div, pre); // Append <pre> block and copy button to container div.appendChild(pre); return div; } handleCodeBlockExpansion(pre) { if (!_SettingsStore.default.getValue("expandCodeByDefault")) { pre.className = "mx_EventTile_collapsedCodeBlock"; } } addLineNumbers(pre) { // Calculate number of lines in pre const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length; const lineNumbers = document.createElement("span"); lineNumbers.className = "mx_EventTile_lineNumbers"; // Iterate through lines starting with 1 (number of the first line is 1) for (let i = 1; i <= number; i++) { const s = document.createElement("span"); s.textContent = i.toString(); lineNumbers.appendChild(s); } pre.prepend(lineNumbers); pre.append(document.createElement("span")); } async highlightCode(code) { const { default: highlight } = await Promise.resolve().then(() => _interopRequireWildcard(require("highlight.js"))); if (code.textContent && code.textContent.length > MAX_HIGHLIGHT_LENGTH) { console.log("Code block is bigger than highlight limit (" + code.textContent.length + " > " + MAX_HIGHLIGHT_LENGTH + "): not highlighting"); return; } let advertisedLang; for (const cl of code.className.split(/\s+/)) { if (cl.startsWith("language-")) { const maybeLang = cl.split("-", 2)[1]; if (highlight.getLanguage(maybeLang)) { advertisedLang = maybeLang; break; } } } if (advertisedLang) { // If the code says what language it is, highlight it in that language // We don't use highlightElement here because we can't force language detection // off. It should use the one we've found in the CSS class but we'd rather pass // it in explicitly to make sure. code.innerHTML = highlight.highlight(code.textContent ?? "", { language: advertisedLang }).value; } else if (_SettingsStore.default.getValue("enableSyntaxHighlightLanguageDetection") && code.parentElement instanceof HTMLPreElement) { // User has language detection enabled and the code is within a pre // we only auto-highlight if the code block is in a pre), so highlight // the block with auto-highlighting enabled. // We pass highlightjs the text to highlight rather than letting it // work on the DOM with highlightElement because that also adds CSS // classes to the pre/code element that we don't want (the CSS // conflicts with our own). code.innerHTML = highlight.highlightAuto(code.textContent ?? "").value; } } componentDidUpdate(prevProps) { if (!this.props.editState) { const stoppedEditing = prevProps.editState && !this.props.editState; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; if (messageWasEdited || stoppedEditing) { this.applyFormatting(); } } } componentWillUnmount() { this.unmounted = true; (0, _pillify.unmountPills)(this.pills); (0, _tooltipify.unmountTooltips)(this.tooltips); this.pills = []; this.tooltips = []; } shouldComponentUpdate(nextProps, nextState) { //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); // exploit that events are immutable :) return nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || nextProps.showUrlPreview !== this.props.showUrlPreview || nextProps.editState !== this.props.editState || nextState.links !== this.state.links || nextState.widgetHidden !== this.state.widgetHidden || nextProps.isSeeingThroughMessageHiddenForModeration !== this.props.isSeeingThroughMessageHiddenForModeration; } calculateUrlPreview() { //console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); if (this.props.showUrlPreview && this.contentRef.current) { // pass only the first child which is the event tile otherwise this recurses on edited events let links = this.findLinks([this.contentRef.current]); if (links.length) { // de-duplicate the links using a set here maintains the order links = Array.from(new Set(links)); this.setState({ links }); // lazy-load the hidden state of the preview widget from localstorage if (window.localStorage) { const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); this.setState({ widgetHidden: hidden }); } } else if (this.state.links.length) { this.setState({ links: [] }); } } } activateSpoilers(nodes) { let node = nodes[0]; while (node) { if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") { const spoilerContainer = document.createElement("span"); const reason = node.getAttribute("data-mx-spoiler") ?? undefined; node.removeAttribute("data-mx-spoiler"); // we don't want to recurse const spoiler = /*#__PURE__*/_react.default.createElement(_compoundWeb.TooltipProvider, null, /*#__PURE__*/_react.default.createElement(_Spoiler.default, { reason: reason, contentHtml: node.outerHTML })); _reactDom.default.render(spoiler, spoilerContainer); node.parentNode?.replaceChild(spoilerContainer, node); node = spoilerContainer; } if (node.childNodes && node.childNodes.length) { this.activateSpoilers(node.childNodes); } node = node.nextSibling; } } findLinks(nodes) { let links = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.tagName === "A" && node.getAttribute("href")) { if (this.isLinkPreviewable(node)) { links.push(node.getAttribute("href")); } } else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") { continue; } else if (node.children && node.children.length) { links = links.concat(this.findLinks(node.children)); } } return links; } isLinkPreviewable(node) { // don't try to preview relative links const href = node.getAttribute("href") ?? ""; if (!href.startsWith("http://") && !href.startsWith("https://")) { return false; } const url = node.getAttribute("href"); const host = url?.match(/^https?:\/\/(.*?)(\/|$)/)?.[1]; // never preview permalinks (if anything we should give a smart // preview of the room/user they point to: nobody needs to be reminded // what the matrix.to site looks like). if (!host || (0, _Permalinks.isPermalinkHost)(host)) return false; // as a random heuristic to avoid highlighting things like "foo.pl" // we require the linked text to either include a / (either from http:// // or from a full foo.bar/baz style schemeless URL) - or be a markdown-style // link, in which case we check the target text differs from the link value. // TODO: make this configurable? if (node.textContent?.includes("/")) { return true; } if (node.textContent?.toLowerCase().trim().startsWith(host.toLowerCase())) { // it's a "foo.pl" style link return false; } else { // it's a [foo bar](http://foo.com) style link return true; } } renderEditedMarker() { const date = this.props.mxEvent.replacingEventDate(); const dateString = date && (0, _DateUtils.formatDate)(date); return /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { className: "mx_EventTile_edited", onClick: this.openHistoryDialog, "aria-label": (0, _languageHandler._t)("timeline|edits|tooltip_label", { date: dateString }), title: (0, _languageHandler._t)("timeline|edits|tooltip_title", { date: dateString }), caption: (0, _languageHandler._t)("timeline|edits|tooltip_sub") }, /*#__PURE__*/_react.default.createElement("span", null, `(${(0, _languageHandler._t)("common|edited")})`)); } /** * Render a marker informing the user that, while they can see the message, * it is hidden for other users. */ renderPendingModerationMarker() { let text; const visibility = this.props.mxEvent.messageVisibility(); switch (visibility.visible) { case true: throw new Error("renderPendingModerationMarker should only be applied to hidden messages"); case false: if (visibility.reason) { text = (0, _languageHandler._t)("timeline|pending_moderation_reason", { reason: visibility.reason }); } else { text = (0, _languageHandler._t)("timeline|pending_moderation"); } break; } return /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_pendingModeration" }, `(${text})`); } render() { if (this.props.editState) { const isWysiwygComposerEnabled = _SettingsStore.default.getValue("feature_wysiwyg_composer"); return isWysiwygComposerEnabled ? /*#__PURE__*/_react.default.createElement(_wysiwyg_composer.EditWysiwygComposer, { editorStateTransfer: this.props.editState, className: "mx_EventTile_content" }) : /*#__PURE__*/_react.default.createElement(_EditMessageComposer.default, { editState: this.props.editState, className: "mx_EventTile_content" }); } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); const isNotice = content.msgtype === _matrix.MsgType.Notice; const isEmote = content.msgtype === _matrix.MsgType.Emote; const isCaption = [_matrix.MsgType.Image, _matrix.MsgType.File, _matrix.MsgType.Audio, _matrix.MsgType.Video].includes(content.msgtype); const willHaveWrapper = this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote; // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!(0, _Reply.getParentEventId)(mxEvent); const htmlOpts = { disableBigEmoji: isEmote || !_SettingsStore.default.getValue("TextualBody.enableBigEmoji"), // Part of Replies fallback support stripReplyFallback: stripReply }; let body = willHaveWrapper ? HtmlUtils.bodyToSpan(content, this.props.highlights, htmlOpts, this.contentRef, false) : HtmlUtils.bodyToDiv(content, this.props.highlights, htmlOpts, this.contentRef); if (this.props.replacingEventId) { body = /*#__PURE__*/_react.default.createElement("div", { dir: "auto", className: "mx_EventTile_annotated" }, body, this.renderEditedMarker()); } if (this.props.isSeeingThroughMessageHiddenForModeration) { body = /*#__PURE__*/_react.default.createElement("div", { dir: "auto", className: "mx_EventTile_annotated" }, body, this.renderPendingModerationMarker()); } if (this.props.highlightLink) { body = /*#__PURE__*/_react.default.createElement("a", { href: this.props.highlightLink }, body); } else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") { body = /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { kind: "link_inline", onClick: this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"]) }, body); } let widgets; if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { widgets = /*#__PURE__*/_react.default.createElement(_LinkPreviewGroup.default, { links: this.state.links, mxEvent: this.props.mxEvent, onCancelClick: this.onCancelClick, onHeightChanged: this.props.onHeightChanged }); } if (isEmote) { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_MEmoteBody mx_EventTile_content", onClick: this.onBodyLinkClick, dir: "auto" }, "*\xA0", /*#__PURE__*/_react.default.createElement("span", { className: "mx_MEmoteBody_sender", onClick: this.onEmoteSenderClick }, mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()), "\xA0", body, widgets); } if (isNotice) { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_MNoticeBody mx_EventTile_content", onClick: this.onBodyLinkClick }, body, widgets); } if (isCaption) { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_MTextBody mx_EventTile_caption", onClick: this.onBodyLinkClick }, body, widgets); } return /*#__PURE__*/_react.default.createElement("div", { className: "mx_MTextBody mx_EventTile_content", onClick: this.onBodyLinkClick }, body, widgets); } } exports.default = TextualBody; (0, _defineProperty2.default)(TextualBody, "contextType", _RoomContext.default); //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfcmVhY3QiLCJfaW50ZXJvcFJlcXVpcmVXaWxkY2FyZCIsInJlcXVpcmUiLCJfcmVhY3REb20iLCJfaW50ZXJvcFJlcXVpcmVEZWZhdWx0IiwiX21hdHJpeCIsIl9jb21wb3VuZFdlYiIsIkh0bWxVdGlscyIsIl9EYXRlVXRpbHMiLCJfTW9kYWwiLCJfZGlzcGF0Y2hlciIsIl9sYW5ndWFnZUhhbmRsZXIiLCJfQ29udGV4dE1lbnUiLCJDb250ZXh0TWVudSIsIl9TZXR0aW5nc1N0b3JlIiwiX3BpbGxpZnkiLCJfdG9vbHRpcGlmeSIsIl9JbnRlZ3JhdGlvbk1hbmFnZXJzIiwiX1Blcm1hbGlua3MiLCJfc3RyaW5ncyIsIl9VSVN0b3JlIiwiX2FjdGlvbnMiLCJfR2VuZXJpY1RleHRDb250ZXh0TWVudSIsIl9TcG9pbGVyIiwiX1F1ZXN0aW9uRGlhbG9nIiwiX01lc3NhZ2VFZGl0SGlzdG9yeURpYWxvZyIsIl9FZGl0TWVzc2FnZUNvbXBvc2VyIiwiX0xpbmtQcmV2aWV3R3JvdXAiLCJfUm9vbUNvbnRleHQiLCJfQWNjZXNzaWJsZUJ1dHRvbiIsIl9saW5raWZ5TWF0cml4IiwiX1JlcGx5IiwiX3d5c2l3eWdfY29tcG9zZXIiLCJfTWF0cml4Q2xpZW50UGVnIiwiX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlIiwiZSIsIldlYWtNYXAiLCJyIiwidCIsIl9fZXNNb2R1bGUiLCJkZWZhdWx0IiwiaGFzIiwiZ2V0IiwibiIsIl9fcHJvdG9fXyIsImEiLCJPYmplY3QiLCJkZWZpbmVQcm9wZXJ0eSIsImdldE93blByb3BlcnR5RGVzY3JpcHRvciIsInUiLCJoYXNPd25Qcm9wZXJ0eSIsImNhbGwiLCJpIiwic2V0Iiwib3duS2V5cyIsImtleXMiLCJnZXRPd25Qcm9wZXJ0eVN5bWJvbHMiLCJvIiwiZmlsdGVyIiwiZW51bWVyYWJsZSIsInB1c2giLCJhcHBseSIsIl9vYmplY3RTcHJlYWQiLCJhcmd1bWVudHMiLCJsZW5ndGgiLCJmb3JFYWNoIiwiX2RlZmluZVByb3BlcnR5MiIsImdldE93blByb3BlcnR5RGVzY3JpcHRvcnMiLCJkZWZpbmVQcm9wZXJ0aWVzIiwiTUFYX0hJR0hMSUdIVF9MRU5HVEgiLCJUZXh0dWFsQm9keSIsIlJlYWN0IiwiQ29tcG9uZW50IiwiY29uc3RydWN0b3IiLCJhcmdzIiwiY3JlYXRlUmVmIiwibGlua3MiLCJ3aWRnZXRIaWRkZW4iLCJzZXRTdGF0ZSIsImdsb2JhbCIsImxvY2FsU3RvcmFnZSIsInNldEl0ZW0iLCJwcm9wcyIsIm14RXZlbnQiLCJnZXRJZCIsImZvcmNlVXBkYXRlIiwiZGlzIiwiZGlzcGF0Y2giLCJhY3Rpb24iLCJBY3Rpb24iLCJDb21wb3Nlckluc2VydCIsInVzZXJJZCIsImdldFNlbmRlciIsInRpbWVsaW5lUmVuZGVyaW5nVHlwZSIsImNvbnRleHQiLCJ0YXJnZXQiLCJjbGFzc0xpc3QiLCJjb250YWlucyIsImxpbmtpZnlPcHRzIiwiY2xhc3NOYW1lIiwibm9kZU5hbWUiLCJjbG9zZXN0IiwibG9jYWxIcmVmIiwidHJ5VHJhbnNmb3JtUGVybWFsaW5rVG9Mb2NhbEhyZWYiLCJocmVmIiwicHJldmVudERlZmF1bHQiLCJ3aW5kb3ciLCJsb2NhdGlvbiIsImhhc2giLCJpc1dpZGdldEhpZGRlbiIsInN0YXRlIiwidW5oaWRlV2lkZ2V0IiwicmVtb3ZlSXRlbSIsInN0YXJ0ZXJMaW5rIiwiZXYiLCJtYW5hZ2VycyIsIkludGVncmF0aW9uTWFuYWdlcnMiLCJzaGFyZWRJbnN0YW5jZSIsImhhc01hbmFnZXIiLCJvcGVuTm9NYW5hZ2VyRGlhbG9nIiwiaW50ZWdyYXRpb25NYW5hZ2VyIiwiZ2V0UHJpbWFyeU1hbmFnZXIiLCJzY2FsYXJDbGllbnQiLCJnZXRTY2FsYXJDbGllbnQiLCJjb25uZWN0IiwidGhlbiIsImNvbXBsZXRlVXJsIiwiZ2V0U3RhcnRlckxpbmsiLCJpbnRlZ3JhdGlvbnNVcmwiLCJ1aVVybCIsIk1vZGFsIiwiY3JlYXRlRGlhbG9nIiwiUXVlc3Rpb25EaWFsb2ciLCJ0aXRsZSIsIl90IiwiZGVzY3JpcHRpb24iLCJjcmVhdGVFbGVtZW50IiwiYnV0dG9uIiwib25GaW5pc2hlZCIsImNvbmZpcm1lZCIsIndpZHRoIiwic2NyZWVuIiwiaGVpZ2h0IiwibGVmdCIsInRvcCIsImZlYXR1cmVzIiwid25kIiwib3BlbiIsIm9wZW5lciIsIk1lc3NhZ2VFZGl0SGlzdG9yeURpYWxvZyIsImNvbXBvbmVudERpZE1vdW50IiwiZWRpdFN0YXRlIiwiYXBwbHlGb3JtYXR0aW5nIiwiY29udGVudCIsImNvbnRlbnRSZWYiLCJjdXJyZW50Iiwic2hvd0xpbmVOdW1iZXJzIiwiU2V0dGluZ3NTdG9yZSIsImdldFZhbHVlIiwiYWN0aXZhdGVTcG9pbGVycyIsImxpbmtpZnlFbGVtZW50IiwicGlsbGlmeUxpbmtzIiwiTWF0cml4Q2xpZW50UGVnIiwic2FmZUdldCIsInBpbGxzIiwiY2FsY3VsYXRlVXJsUHJldmlldyIsInRvb2x0aXBpZnlMaW5rcyIsInRvb2x0aXBzIiwiZ2V0Q29udGVudCIsImZvcm1hdCIsInByZXMiLCJSZWFjdERPTSIsImZpbmRET01Ob2RlIiwiZ2V0RWxlbWVudHNCeVRhZ05hbWUiLCJwYXJlbnRFbGVtZW50IiwiYWRkQ29kZUVsZW1lbnQiLCJkaXYiLCJ3cmFwSW5EaXYiLCJoYW5kbGVDb2RlQmxvY2tFeHBhbnNpb24iLCJhZGRDb2RlRXhwYW5zaW9uQnV0dG9uIiwiYWRkQ29kZUNvcHlCdXR0b24iLCJhZGRMaW5lTnVtYmVycyIsImNvZGVzIiwic2V0VGltZW91dCIsInVubW91bnRlZCIsImhpZ2hsaWdodENvZGUiLCJwcmUiLCJjb2RlIiwiZG9jdW1lbnQiLCJhcHBlbmQiLCJjaGlsZE5vZGVzIiwiYXBwZW5kQ2hpbGQiLCJwZXJjZW50YWdlT2ZWaWV3cG9ydCIsIk1hdGgiLCJyb3VuZCIsIm9mZnNldEhlaWdodCIsIlVJU3RvcmUiLCJpbnN0YW5jZSIsIndpbmRvd0hlaWdodCIsIm9uY2xpY2siLCJvbkhlaWdodENoYW5nZWQiLCJleHBhbnNpb25CdXR0b25FeGlzdHMiLCJnZXRFbGVtZW50c0J5Q2xhc3NOYW1lIiwiY29weUNvZGUiLCJzdWNjZXNzZnVsIiwidGV4dENvbnRlbnQiLCJjb3B5UGxhaW50ZXh0IiwiYnV0dG9uUmVjdCIsImdldEJvdW5kaW5nQ2xpZW50UmVjdCIsImNsb3NlIiwiY3JlYXRlTWVudSIsIkdlbmVyaWNUZXh0Q29udGV4dE1lbnUiLCJ0b1JpZ2h0T2YiLCJjaGV2cm9uRmFjZSIsIkNoZXZyb25GYWNlIiwiTm9uZSIsIm1lc3NhZ2UiLCJvbm1vdXNlbGVhdmUiLCJwYXJlbnROb2RlIiwicmVwbGFjZUNoaWxkIiwibnVtYmVyIiwiaW5uZXJIVE1MIiwicmVwbGFjZSIsInNwbGl0IiwibGluZU51bWJlcnMiLCJzIiwidG9TdHJpbmciLCJwcmVwZW5kIiwiaGlnaGxpZ2h0IiwiUHJvbWlzZSIsInJlc29sdmUiLCJjb25zb2xlIiwibG9nIiwiYWR2ZXJ0aXNlZExhbmciLCJjbCIsInN0YXJ0c1dpdGgiLCJtYXliZUxhbmciLCJnZXRMYW5ndWFnZSIsImxhbmd1YWdlIiwidmFsdWUiLCJIVE1MUHJlRWxlbWVudCIsImhpZ2hsaWdodEF1dG8iLCJjb21wb25lbnREaWRVcGRhdGUiLCJwcmV2UHJvcHMiLCJzdG9wcGVkRWRpdGluZyIsIm1lc3NhZ2VXYXNFZGl0ZWQiLCJyZXBsYWNpbmdFdmVudElkIiwiY29tcG9uZW50V2lsbFVubW91bnQiLCJ1bm1vdW50UGlsbHMiLCJ1bm1vdW50VG9vbHRpcHMiLCJzaG91bGRDb21wb25lbnRVcGRhdGUiLCJuZXh0UHJvcHMiLCJuZXh0U3RhdGUiLCJoaWdobGlnaHRzIiwiaGlnaGxpZ2h0TGluayIsInNob3dVcmxQcmV2aWV3IiwiaXNTZWVpbmdUaHJvdWdoTWVzc2FnZUhpZGRlbkZvck1vZGVyYXRpb24iLCJmaW5kTGlua3MiLCJBcnJheSIsImZyb20iLCJTZXQiLCJoaWRkZW4iLCJnZXRJdGVtIiwibm9kZXMiLCJub2RlIiwidGFnTmFtZSIsImdldEF0dHJpYnV0ZSIsInNwb2lsZXJDb250YWluZXIiLCJyZWFzb24iLCJ1bmRlZmluZWQiLCJyZW1vdmVBdHRyaWJ1dGUiLCJzcG9pbGVyIiwiVG9vbHRpcFByb3ZpZGVyIiwiY29udGVudEh0bWwiLCJvdXRlckhUTUwiLCJyZW5kZXIiLCJuZXh0U2libGluZyIsImlzTGlua1ByZXZpZXdhYmxlIiwiY2hpbGRyZW4iLCJjb25jYXQiLCJ1cmwiLCJob3N0IiwibWF0Y2giLCJpc1Blcm1hbGlua0hvc3QiLCJpbmNsdWRlcyIsInRvTG93ZXJDYXNlIiwidHJpbSIsInJlbmRlckVkaXRlZE1hcmtlciIsImRhdGUiLCJyZXBsYWNpbmdFdmVudERhdGUiLCJkYXRlU3RyaW5nIiwiZm9ybWF0RGF0ZSIsIm9uQ2xpY2siLCJvcGVuSGlzdG9yeURpYWxvZyIsImNhcHRpb24iLCJyZW5kZXJQZW5kaW5nTW9kZXJhdGlvbk1hcmtlciIsInRleHQiLCJ2aXNpYmlsaXR5IiwibWVzc2FnZVZpc2liaWxpdHkiLCJ2aXNpYmxlIiwiRXJyb3IiLCJpc1d5c2l3eWdDb21wb3NlckVuYWJsZWQiLCJFZGl0V3lzaXd5Z0NvbXBvc2VyIiwiZWRpdG9yU3RhdGVUcmFuc2ZlciIsImlzTm90aWNlIiwibXNndHlwZSIsIk1zZ1R5cGUiLCJOb3RpY2UiLCJpc0Vtb3RlIiwiRW1vdGUiLCJpc0NhcHRpb24iLCJJbWFnZSIsIkZpbGUiLCJBdWRpbyIsIlZpZGVvIiwid2lsbEhhdmVXcmFwcGVyIiwic3RyaXBSZXBseSIsInJlcGxhY2luZ0V2ZW50IiwiZ2V0UGFyZW50RXZlbnRJZCIsImh0bWxPcHRzIiwiZGlzYWJsZUJpZ0Vtb2ppIiwic3RyaXBSZXBseUZhbGxiYWNrIiwiYm9keSIsImJvZHlUb1NwYW4iLCJib2R5VG9EaXYiLCJkaXIiLCJkYXRhIiwia2luZCIsIm9uU3RhcnRlckxpbmtDbGljayIsImJpbmQiLCJ3aWRnZXRzIiwib25DYW5jZWxDbGljayIsIm9uQm9keUxpbmtDbGljayIsIm9uRW1vdGVTZW5kZXJDbGljayIsInNlbmRlciIsIm5hbWUiLCJleHBvcnRzIiwiUm9vbUNvbnRleHQiXSwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvY29tcG9uZW50cy92aWV3cy9tZXNzYWdlcy9UZXh0dWFsQm9keS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLypcbkNvcHlyaWdodCAyMDI0IE5ldyBWZWN0b3IgTHRkLlxuQ29weXJpZ2h0IDIwMTUtMjAyMSBUaGUgTWF0cml4Lm9yZyBGb3VuZGF0aW9uIEMuSS5DLlxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQgUmVhY3QsIHsgY3JlYXRlUmVmLCBTeW50aGV0aWNFdmVudCwgTW91c2VFdmVudCB9IGZyb20gXCJyZWFjdFwiO1xuaW1wb3J0IFJlYWN0RE9NIGZyb20gXCJyZWFjdC1kb21cIjtcbmltcG9ydCB7IE1zZ1R5cGUgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbWF0cml4XCI7XG5pbXBvcnQgeyBUb29sdGlwUHJvdmlkZXIgfSBmcm9tIFwiQHZlY3Rvci1pbS9jb21wb3VuZC13ZWJcIjtcblxuaW1wb3J0ICogYXMgSHRtbFV0aWxzIGZyb20gXCIuLi8uLi8uLi9IdG1sVXRpbHNcIjtcbmltcG9ydCB7IGZvcm1hdERhdGUgfSBmcm9tIFwiLi4vLi4vLi4vRGF0ZVV0aWxzXCI7XG5pbXBvcnQgTW9kYWwgZnJvbSBcIi4uLy4uLy4uL01vZGFsXCI7XG5pbXBvcnQgZGlzIGZyb20gXCIuLi8uLi8uLi9kaXNwYXRjaGVyL2Rpc3BhdGNoZXJcIjtcbmltcG9ydCB7IF90IH0gZnJvbSBcIi4uLy4uLy4uL2xhbmd1YWdlSGFuZGxlclwiO1xuaW1wb3J0ICogYXMgQ29udGV4dE1lbnUgZnJvbSBcIi4uLy4uL3N0cnVjdHVyZXMvQ29udGV4dE1lbnVcIjtcbmltcG9ydCB7IENoZXZyb25GYWNlLCB0b1JpZ2h0T2YgfSBmcm9tIFwiLi4vLi4vc3RydWN0dXJlcy9Db250ZXh0TWVudVwiO1xuaW1wb3J0IFNldHRpbmdzU3RvcmUgZnJvbSBcIi4uLy4uLy4uL3NldHRpbmdzL1NldHRpbmdzU3RvcmVcIjtcbmltcG9ydCB7IHBpbGxpZnlMaW5rcywgdW5tb3VudFBpbGxzIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL3BpbGxpZnlcIjtcbmltcG9ydCB7IHRvb2x0aXBpZnlMaW5rcywgdW5tb3VudFRvb2x0aXBzIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL3Rvb2x0aXBpZnlcIjtcbmltcG9ydCB7IEludGVncmF0aW9uTWFuYWdlcnMgfSBmcm9tIFwiLi4vLi4vLi4vaW50ZWdyYXRpb25zL0ludGVncmF0aW9uTWFuYWdlcnNcIjtcbmltcG9ydCB7IGlzUGVybWFsaW5rSG9zdCwgdHJ5VHJhbnNmb3JtUGVybWFsaW5rVG9Mb2NhbEhyZWYgfSBmcm9tIFwiLi4vLi4vLi4vdXRpbHMvcGVybWFsaW5rcy9QZXJtYWxpbmtzXCI7XG5pbXBvcnQgeyBjb3B5UGxhaW50ZXh0IH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL3N0cmluZ3NcIjtcbmltcG9ydCBVSVN0b3JlIGZyb20gXCIuLi8uLi8uLi9zdG9yZXMvVUlTdG9yZVwiO1xuaW1wb3J0IHsgQWN0aW9uIH0gZnJvbSBcIi4uLy4uLy4uL2Rpc3BhdGNoZXIvYWN0aW9uc1wiO1xuaW1wb3J0IEdlbmVyaWNUZXh0Q29udGV4dE1lbnUgZnJvbSBcIi4uL2NvbnRleHRfbWVudXMvR2VuZXJpY1RleHRDb250ZXh0TWVudVwiO1xuaW1wb3J0IFNwb2lsZXIgZnJvbSBcIi4uL2VsZW1lbnRzL1Nwb2lsZXJcIjtcbmltcG9ydCBRdWVzdGlvbkRpYWxvZyBmcm9tIFwiLi4vZGlhbG9ncy9RdWVzdGlvbkRpYWxvZ1wiO1xuaW1wb3J0IE1lc3NhZ2VFZGl0SGlzdG9yeURpYWxvZyBmcm9tIFwiLi4vZGlhbG9ncy9NZXNzYWdlRWRpdEhpc3RvcnlEaWFsb2dcIjtcbmltcG9ydCBFZGl0TWVzc2FnZUNvbXBvc2VyIGZyb20gXCIuLi9yb29tcy9FZGl0TWVzc2FnZUNvbXBvc2VyXCI7XG5pbXBvcnQgTGlua1ByZXZpZXdHcm91cCBmcm9tIFwiLi4vcm9vbXMvTGlua1ByZXZpZXdHcm91cFwiO1xuaW1wb3J0IHsgSUJvZHlQcm9wcyB9IGZyb20gXCIuL0lCb2R5UHJvcHNcIjtcbmltcG9ydCBSb29tQ29udGV4dCBmcm9tIFwiLi4vLi4vLi4vY29udGV4dHMvUm9vbUNvbnRleHRcIjtcbmltcG9ydCBBY2Nlc3NpYmxlQnV0dG9uIGZyb20gXCIuLi9lbGVtZW50cy9BY2Nlc3NpYmxlQnV0dG9uXCI7XG5pbXBvcnQgeyBvcHRpb25zIGFzIGxpbmtpZnlPcHRzIH0gZnJvbSBcIi4uLy4uLy4uL2xpbmtpZnktbWF0cml4XCI7XG5pbXBvcnQgeyBnZXRQYXJlbnRFdmVudElkIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL1JlcGx5XCI7XG5pbXBvcnQgeyBFZGl0V3lzaXd5Z0NvbXBvc2VyIH0gZnJvbSBcIi4uL3Jvb21zL3d5c2l3eWdfY29tcG9zZXJcIjtcbmltcG9ydCB7IElFdmVudFRpbGVPcHMgfSBmcm9tIFwiLi4vcm9vbXMvRXZlbnRUaWxlXCI7XG5pbXBvcnQgeyBNYXRyaXhDbGllbnRQZWcgfSBmcm9tIFwiLi4vLi4vLi4vTWF0cml4Q2xpZW50UGVnXCI7XG5cbmNvbnN0IE1BWF9ISUdITElHSFRfTEVOR1RIID0gNDA5NjtcblxuaW50ZXJmYWNlIElTdGF0ZSB7XG4gICAgLy8gdGhlIFVSTHMgKGlmIGFueSkgdG8gYmUgcHJldmlld2VkIHdpdGggYSBMaW5rUHJldmlld1dpZGdldCBpbnNpZGUgdGhpcyBUZXh0dWFsQm9keS5cbiAgICBsaW5rczogc3RyaW5nW107XG5cbiAgICAvLyB0cmFjayB3aGV0aGVyIHRoZSBwcmV2aWV3IHdpZGdldCBpcyBoaWRkZW5cbiAgICB3aWRnZXRIaWRkZW46IGJvb2xlYW47XG59XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFRleHR1YWxCb2R5IGV4dGVuZHMgUmVhY3QuQ29tcG9uZW50PElCb2R5UHJvcHMsIElTdGF0ZT4ge1xuICAgIHByaXZhdGUgcmVhZG9ubHkgY29udGVudFJlZiA9IGNyZWF0ZVJlZjxIVE1MRGl2RWxlbWVudD4oKTtcblxuICAgIHByaXZhdGUgdW5tb3VudGVkID0gZmFsc2U7XG4gICAgcHJpdmF0ZSBwaWxsczogRWxlbWVudFtdID0gW107XG4gICAgcHJpdmF0ZSB0b29sdGlwczogRWxlbWVudFtdID0gW107XG5cbiAgICBwdWJsaWMgc3RhdGljIGNvbnRleHRUeXBlID0gUm9vbUNvbnRleHQ7XG4gICAgcHVibGljIGRlY2xhcmUgY29udGV4dDogUmVhY3QuQ29udGV4dFR5cGU8dHlwZW9mIFJvb21Db250ZXh0PjtcblxuICAgIHB1YmxpYyBzdGF0ZSA9IHtcbiAgICAgICAgbGlua3M6IFtdLFxuICAgICAgICB3aWRnZXRIaWRkZW46IGZhbHNlLFxuICAgIH07XG5cbiAgICBwdWJsaWMgY29tcG9uZW50RGlkTW91bnQoKTogdm9pZCB7XG4gICAgICAgIGlmICghdGhpcy5wcm9wcy5lZGl0U3RhdGUpIHtcbiAgICAgICAgICAgIHRoaXMuYXBwbHlGb3JtYXR0aW5nKCk7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBwcml2YXRlIGFwcGx5Rm9ybWF0dGluZygpOiB2b2lkIHtcbiAgICAgICAgLy8gRnVuY3Rpb24gaXMgb25seSBjYWxsZWQgZnJvbSByZW5kZXIgLyBjb21wb25lbnREaWRNb3VudCDihpIgY29udGVudFJlZiBpcyBzZXRcbiAgICAgICAgY29uc3QgY29udGVudCA9IHRoaXMuY29udGVudFJlZi5jdXJyZW50ITtcblxuICAgICAgICBjb25zdCBzaG93TGluZU51bWJlcnMgPSBTZXR0aW5nc1N0b3JlLmdldFZhbHVlKFwic2hvd0NvZGVMaW5lTnVtYmVyc1wiKTtcbiAgICAgICAgdGhpcy5hY3RpdmF0ZVNwb2lsZXJzKFtjb250ZW50XSk7XG5cbiAgICAgICAgSHRtbFV0aWxzLmxpbmtpZnlFbGVtZW50KGNvbnRlbnQpO1xuICAgICAgICBwaWxsaWZ5TGlua3MoTWF0cml4Q2xpZW50UGVnLnNhZmVHZXQoKSwgW2NvbnRlbnRdLCB0aGlzLnByb3BzLm14RXZlbnQsIHRoaXMucGlsbHMpO1xuXG4gICAgICAgIHRoaXMuY2FsY3VsYXRlVXJsUHJldmlldygpO1xuXG4gICAgICAgIC8vIHRvb2x0aXBpZnlMaW5rcyBBRlRFUiBjYWxjdWxhdGVVcmxQcmV2aWV3IGJlY2F1c2UgdGhlIERPTSBpbnNpZGUgdGhlIHRvb2x0aXBcbiAgICAgICAgLy8gY29udGFpbmVyIGlzIGVtcHR5IGJlZm9yZSB0aGUgaW50ZXJuYWwgY29tcG9uZW50IGhhcyBtb3VudGVkIHNvIGNhbGN1bGF0ZVVybFByZXZpZXdcbiAgICAgICAgLy8gd29uJ3QgZmluZCBhbnkgYW5jaG9yc1xuICAgICAgICB0b29sdGlwaWZ5TGlua3MoW2NvbnRlbnRdLCB0aGlzLnBpbGxzLCB0aGlzLnRvb2x0aXBzKTtcblxuICAgICAgICBpZiAodGhpcy5wcm9wcy5teEV2ZW50LmdldENvbnRlbnQoKS5mb3JtYXQgPT09IFwib3JnLm1hdHJpeC5jdXN0b20uaHRtbFwiKSB7XG4gICAgICAgICAgICAvLyBIYW5kbGUgZXhwYW5zaW9uIGFuZCBhZGQgYnV0dG9uc1xuICAgICAgICAgICAgY29uc3QgcHJlcyA9IChSZWFjdERPTS5maW5kRE9NTm9kZSh0aGlzKSBhcyBFbGVtZW50KS5nZXRFbGVtZW50c0J5VGFnTmFtZShcInByZVwiKTtcbiAgICAgICAgICAgIGlmIChwcmVzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICAgICAgICBmb3IgKGxldCBpID0gMDsgaSA8IHByZXMubGVuZ3RoOyBpKyspIHtcbiAgICAgICAgICAgICAgICAgICAgLy8gSWYgdGhlcmUgYWxyZWFkeSBpcyBhIGRpdiB3cmFwcGluZyB0aGUgY29kZWJsb2NrIHdlIHdhbnQgdG8gc2tpcCB0aGlzLlxuICAgICAgICAgICAgICAgICAgICAvLyBUaGlzIGhhcHBlbnMgYWZ0ZXIgdGhlIGNvZGVibG9jayB3YXMgZWRpdGVkLlxuICAgICAgICAgICAgICAgICAgICBpZiAocHJlc1tpXS5wYXJlbnRFbGVtZW50Py5jbGFzc05hbWUgPT0gXCJteF9FdmVudFRpbGVfcHJlX2NvbnRhaW5lclwiKSBjb250aW51ZTtcbiAgICAgICAgICAgICAgICAgICAgLy8gQWRkIGNvZGUgZWxlbWVudCBpZiBpdCdzIG1pc3Npbmcgc2luY2Ugd2UgZGVwZW5kIG9uIGl0XG4gICAgICAgICAgICAgICAgICAgIGlmIChwcmVzW2ldLmdldEVsZW1lbnRzQnlUYWdOYW1lKFwiY29kZVwiKS5sZW5ndGggPT0gMCkge1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5hZGRDb2RlRWxlbWVudChwcmVzW2ldKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICAvLyBXcmFwIGEgZGl2IGFyb3VuZCA8cHJlPiBzbyB0aGF0IHRoZSBjb3B5IGJ1dHRvbiBjYW4gYmUgY29ycmVjdGx5IHBvc2l0aW9uZWRcbiAgICAgICAgICAgICAgICAgICAgLy8gd2hlbiB0aGUgPHByZT4gb3ZlcmZsb3dzIGFuZCBpcyBzY3JvbGxlZCBob3Jpem9udGFsbHkuXG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IGRpdiA9IHRoaXMud3JhcEluRGl2KHByZXNbaV0pO1xuICAgICAgICAgICAgICAgICAgICB0aGlzLmhhbmRsZUNvZGVCbG9ja0V4cGFuc2lvbihwcmVzW2ldKTtcbiAgICAgICAgICAgICAgICAgICAgdGhpcy5hZGRDb2RlRXhwYW5zaW9uQnV0dG9uKGRpdiwgcHJlc1tpXSk7XG4gICAgICAgICAgICAgICAgICAgIHRoaXMuYWRkQ29kZUNvcHlCdXR0b24oZGl2KTtcbiAgICAgICAgICAgICAgICAgICAgaWYgKHNob3dMaW5lTnVtYmVycykge1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5hZGRMaW5lTnVtYmVycyhwcmVzW2ldKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC8vIEhpZ2hsaWdodCBjb2RlXG4gICAgICAgICAgICBjb25zdCBjb2RlcyA9IChSZWFjdERPTS5maW5kRE9NTm9kZSh0aGlzKSBhcyBFbGVtZW50KS5nZXRFbGVtZW50c0J5VGFnTmFtZShcImNvZGVcIik7XG4gICAgICAgICAgICBpZiAoY29kZXMubGVuZ3RoID4gMCkge1xuICAgICAgICAgICAgICAgIC8vIERvIHRoaXMgYXN5bmNocm9ub3VzbHk6IHBhcnNpbmcgY29kZSB0YWtlcyB0aW1lIGFuZCB3ZSBkb24ndFxuICAgICAgICAgICAgICAgIC8vIG5lZWQgdG8gYmxvY2sgdGhlIERPTSB1cGRhdGUgb24gaXQuXG4gICAgICAgICAgICAgICAgd2luZG93LnNldFRpbWVvdXQoKCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICBpZiAodGhpcy51bm1vdW50ZWQpIHJldHVybjtcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBjb2Rlcy5sZW5ndGg7IGkrKykge1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5oaWdobGlnaHRDb2RlKGNvZGVzW2ldKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH0sIDEwKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cblxuICAgIHByaXZhdGUgYWRkQ29kZUVsZW1lbnQocHJlOiBIVE1MUHJlRWxlbWVudCk6IHZvaWQge1xuICAgICAgICBjb25zdCBjb2RlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImNvZGVcIik7XG4gICAgICAgIGNvZGUuYXBwZW5kKC4uLnByZS5jaGlsZE5vZGVzKTtcbiAgICAgICAgcHJlLmFwcGVuZENoaWxkKGNvZGUpO1xuICAgIH1cblxuICAgIHByaXZhdGUgYWRkQ29kZUV4cGFuc2lvbkJ1dHRvbihkaXY6IEhUTUxEaXZFbGVtZW50LCBwcmU6IEhUTUxQcmVFbGVtZW50KTogdm9pZCB7XG4gICAgICAgIC8vIENhbGN1bGF0ZSBob3cgbWFueSBwZXJjZW50IGRvZXMgdGhlIHByZSBlbGVtZW50IHRha2UgdXAuXG4gICAgICAgIC8vIElmIGl0J3MgbGVzcyB0aGFuIDMwJSB3ZSBkb24ndCBhZGQgdGhlIGV4cGFuc2lvbiBidXR0b24uXG4gICAgICAgIC8vIFdlIGFsc28gcm91bmQgdGhlIG51bWJlciBhcyBpdCBzb21ldGltZXMgY2FuIGJlIDI5Ljk5Li4uXG4gICAgICAgIGNvbnN0IHBlcmNlbnRhZ2VPZlZpZXdwb3J0ID0gTWF0aC5yb3VuZCgocHJlLm9mZnNldEhlaWdodCAvIFVJU3RvcmUuaW5zdGFuY2Uud2luZG93SGVpZ2h0KSAqIDEwMCk7XG4gICAgICAgIC8vIFRPRE86IGFkZGl0aW9uYWxseSBzaG93IHRoZSBidXR0b24gaWYgaXQncyBhbiBleHBhbmRlZCBxdW90ZWQgbWVzc2FnZVxuICAgICAgICBpZiAocGVyY2VudGFnZU9mVmlld3BvcnQgPCAzMCkgcmV0dXJuO1xuXG4gICAgICAgIGNvbnN0IGJ1dHRvbiA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJzcGFuXCIpO1xuICAgICAgICBidXR0b24uY2xhc3NOYW1lID0gXCJteF9FdmVudFRpbGVfYnV0dG9uIFwiO1xuICAgICAgICBpZiAocHJlLmNsYXNzTmFtZSA9PSBcIm14X0V2ZW50VGlsZV9jb2xsYXBzZWRDb2RlQmxvY2tcIikge1xuICAgICAgICAgICAgYnV0dG9uLmNsYXNzTmFtZSArPSBcIm14X0V2ZW50VGlsZV9leHBhbmRCdXR0b25cIjtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGJ1dHRvbi5jbGFzc05hbWUgKz0gXCJteF9FdmVudFRpbGVfY29sbGFwc2VCdXR0b25cIjtcbiAgICAgICAgfVxuXG4gICAgICAgIGJ1dHRvbi5vbmNsaWNrID0gYXN5bmMgKCk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICAgICAgYnV0dG9uLmNsYXNzTmFtZSA9IFwibXhfRXZlbnRUaWxlX2J1dHRvbiBcIjtcbiAgICAgICAgICAgIGlmIChwcmUuY2xhc3NOYW1lID09IFwibXhfRXZlbnRUaWxlX2NvbGxhcHNlZENvZGVCbG9ja1wiKSB7XG4gICAgICAgICAgICAgICAgcHJlLmNsYXNzTmFtZSA9IFwiXCI7XG4gICAgICAgICAgICAgICAgYnV0dG9uLmNsYXNzTmFtZSArPSBcIm14X0V2ZW50VGlsZV9jb2xsYXBzZUJ1dHRvblwiO1xuICAgICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgICAgICBwcmUuY2xhc3NOYW1lID0gXCJteF9FdmVudFRpbGVfY29sbGFwc2VkQ29kZUJsb2NrXCI7XG4gICAgICAgICAgICAgICAgYnV0dG9uLmNsYXNzTmFtZSArPSBcIm14X0V2ZW50VGlsZV9leHBhbmRCdXR0b25cIjtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQnkgZXhwYW5kaW5nL2NvbGxhcHNpbmcgd2UgY2hhbmdlZFxuICAgICAgICAgICAgLy8gdGhlIGhlaWdodCwgdGhlcmVmb3JlIHdlIGNhbGwgdGhpc1xuICAgICAgICAgICAgdGhpcy5wcm9wcy5vbkhlaWdodENoYW5nZWQ/LigpO1xuICAgICAgICB9O1xuXG4gICAgICAgIGRpdi5hcHBlbmRDaGlsZChidXR0b24pO1xuICAgIH1cblxuICAgIHByaXZhdGUgYWRkQ29kZUNvcHlCdXR0b24oZGl2OiBIVE1MRGl2RWxlbWVudCk6IHZvaWQge1xuICAgICAgICBjb25zdCBidXR0b24gPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwic3BhblwiKTtcbiAgICAgICAgYnV0dG9uLmNsYXNzTmFtZSA9IFwibXhfRXZlbnRUaWxlX2J1dHRvbiBteF9FdmVudFRpbGVfY29weUJ1dHRvbiBcIjtcblxuICAgICAgICAvLyBDaGVjayBpZiBleHBhbnNpb24gYnV0dG9uIGV4aXN0cy4gSWYgc28gd2UgcHV0IHRoZSBjb3B5IGJ1dHRvbiB0byB0aGUgYm90dG9tXG4gICAgICAgIGNvbnN0IGV4cGFuc2lvbkJ1dHRvbkV4aXN0cyA9IGRpdi5nZXRFbGVtZW50c0J5Q2xhc3NOYW1lKFwibXhfRXZlbnRUaWxlX2J1dHRvblwiKTtcbiAgICAgICAgaWYgKGV4cGFuc2lvbkJ1dHRvbkV4aXN0cy5sZW5ndGggPiAwKSBidXR0b24uY2xhc3NOYW1lICs9IFwibXhfRXZlbnRUaWxlX2J1dHRvbkJvdHRvbVwiO1xuXG4gICAgICAgIGJ1dHRvbi5vbmNsaWNrID0gYXN5bmMgKCk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICAgICAgY29uc3QgY29weUNvZGUgPSBidXR0b24ucGFyZW50RWxlbWVudD8uZ2V0RWxlbWVudHNCeVRhZ05hbWUoXCJjb2RlXCIpWzBdO1xuICAgICAgICAgICAgY29uc3Qgc3VjY2Vzc2Z1bCA9IGNvcHlDb2RlPy50ZXh0Q29udGVudCA/IGF3YWl0IGNvcHlQbGFpbnRleHQoY29weUNvZGUudGV4dENvbnRlbnQpIDogZmFsc2U7XG5cbiAgICAgICAgICAgIGNvbnN0IGJ1dHRvblJlY3QgPSBidXR0b24uZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gICAgICAgICAgICBjb25zdCB7IGNsb3NlIH0gPSBDb250ZXh0TWVudS5jcmVhdGVNZW51KEdlbmVyaWNUZXh0Q29udGV4dE1lbnUsIHtcbiAgICAgICAgICAgICAgICAuLi50b1JpZ2h0T2YoYnV0dG9uUmVjdCwgMCksXG4gICAgICAgICAgICAgICAgY2hldnJvbkZhY2U6IENoZXZyb25GYWNlLk5vbmUsXG4gICAgICAgICAgICAgICAgbWVzc2FnZTogc3VjY2Vzc2Z1bCA/IF90KFwiY29tbW9ufGNvcGllZFwiKSA6IF90KFwiZXJyb3J8ZmFpbGVkX2NvcHlcIiksXG4gICAgICAgICAgICB9KTtcbiAgICAgICAgICAgIGJ1dHRvbi5vbm1vdXNlbGVhdmUgPSBjbG9zZTtcbiAgICAgICAgfTtcblxuICAgICAgICBkaXYuYXBwZW5kQ2hpbGQoYnV0dG9uKTtcbiAgICB9XG5cbiAgICBwcml2YXRlIHdyYXBJbkRpdihwcmU6IEhUTUxQcmVFbGVtZW50KTogSFRNTERpdkVsZW1lbnQge1xuICAgICAgICBjb25zdCBkaXYgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuICAgICAgICBkaXYuY2xhc3NOYW1lID0gXCJteF9FdmVudFRpbGVfcHJlX2NvbnRhaW5lclwiO1xuXG4gICAgICAgIC8vIEluc2VydCBjb250YWluaW5nIGRpdiBpbiBwbGFjZSBvZiA8cHJlPiBibG9ja1xuICAgICAgICBwcmUucGFyZW50Tm9kZT8ucmVwbGFjZUNoaWxkKGRpdiwgcHJlKTtcbiAgICAgICAgLy8gQXBwZW5kIDxwcmU+IGJsb2NrIGFuZCBjb3B5IGJ1dHRvbiB0byBjb250YWluZXJcbiAgICAgICAgZGl2LmFwcGVuZENoaWxkKHByZSk7XG5cbiAgICAgICAgcmV0dXJuIGRpdjtcbiAgICB9XG5cbiAgICBwcml2YXRlIGhhbmRsZUNvZGVCbG9ja0V4cGFuc2lvbihwcmU6IEhUTUxQcmVFbGVtZW50KTogdm9pZCB7XG4gICAgICAgIGlmICghU2V0dGluZ3NTdG9yZS5nZXRWYWx1ZShcImV4cGFuZENvZGVCeURlZmF1bHRcIikpIHtcbiAgICAgICAgICAgIHByZS5jbGFzc05hbWUgPSBcIm14X0V2ZW50VGlsZV9jb2xsYXBzZWRDb2RlQmxvY2tcIjtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIHByaXZhdGUgYWRkTGluZU51bWJlcnMocHJlOiBIVE1MUHJlRWxlbWVudCk6IHZvaWQge1xuICAgICAgICAvLyBDYWxjdWxhdGUgbnVtYmVyIG9mIGxpbmVzIGluIHByZVxuICAgICAgICBjb25zdCBudW1iZXIgPSBwcmUuaW5uZXJIVE1MLnJlcGxhY2UoL1xcbig8XFwvY29kZT4pPyQvLCBcIlwiKS5zcGxpdCgvXFxuLykubGVuZ3RoO1xuICAgICAgICBjb25zdCBsaW5lTnVtYmVycyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJzcGFuXCIpO1xuICAgICAgICBsaW5lTnVtYmVycy5jbGFzc05hbWUgPSBcIm14X0V2ZW50VGlsZV9saW5lTnVtYmVyc1wiO1xuICAgICAgICAvLyBJdGVyYXRlIHRocm91Z2ggbGluZXMgc3RhcnRpbmcgd2l0aCAxIChudW1iZXIgb2YgdGhlIGZpcnN0IGxpbmUgaXMgMSlcbiAgICAgICAgZm9yIChsZXQgaSA9IDE7IGkgPD0gbnVtYmVyOyBpKyspIHtcbiAgICAgICAgICAgIGNvbnN0IHMgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwic3BhblwiKTtcbiAgICAgICAgICAgIHMudGV4dENvbnRlbnQgPSBpLnRvU3RyaW5nKCk7XG4gICAgICAgICAgICBsaW5lTnVtYmVycy5hcHBlbmRDaGlsZChzKTtcbiAgICAgICAgfVxuICAgICAgICBwcmUucHJlcGVuZChsaW5lTnVtYmVycyk7XG4gICAgICAgIHByZS5hcHBlbmQoZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcInNwYW5cIikpO1xuICAgIH1cblxuICAgIHByaXZhdGUgYXN5bmMgaGlnaGxpZ2h0Q29kZShjb2RlOiBIVE1MRWxlbWVudCk6IFByb21pc2U8dm9pZD4ge1xuICAgICAgICBjb25zdCB7IGRlZmF1bHQ6IGhpZ2hsaWdodCB9ID0gYXdhaXQgaW1wb3J0KFwiaGlnaGxpZ2h0LmpzXCIpO1xuXG4gICAgICAgIGlmIChjb2RlLnRleHRDb250ZW50ICYmIGNvZGUudGV4dENvbnRlbnQubGVuZ3RoID4gTUFYX0hJR0hMSUdIVF9MRU5HVEgpIHtcbiAgICAgICAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICAgICAgICAgIFwiQ29kZSBibG9jayBpcyBiaWdnZXIgdGhhbiBoaWdobGlnaHQgbGltaXQgKFwiICtcbiAgICAgICAgICAgICAgICAgICAgY29kZS50ZXh0Q29udGVudC5sZW5ndGggK1xuICAgICAgICAgICAgICAgICAgICBcIiA+IFwiICtcbiAgICAgICAgICAgICAgICAgICAgTUFYX0hJR0hMSUdIVF9MRU5HVEggK1xuICAgICAgICAgICAgICAgICAgICBcIik6IG5vdCBoaWdobGlnaHRpbmdcIixcbiAgICAgICAgICAgICk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICBsZXQgYWR2ZXJ0aXNlZExhbmc7XG4gICAgICAgIGZvciAoY29uc3QgY2wgb2YgY29kZS5jbGFzc05hbWUuc3BsaXQoL1xccysvKSkge1xuICAgICAgICAgICAgaWYgKGNsLnN0YXJ0c1dpdGgoXCJsYW5ndWFnZS1cIikpIHtcbiAgICAgICAgICAgICAgICBjb25zdCBtYXliZUxhbmcgPSBjbC5zcGxpdChcIi1cIiwgMilbMV07XG4gICAgICAgICAgICAgICAgaWYgKGhpZ2hsaWdodC5nZXRMYW5ndWFnZShtYXliZUxhbmcpKSB7XG4gICAgICAgICAgICAgICAgICAgIGFkdmVydGlzZWRMYW5nID0gbWF5YmVMYW5nO1xuICAgICAgICAgICAgICAgICAgICBicmVhaztcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoYWR2ZXJ0aXNlZExhbmcpIHtcbiAgICAgICAgICAgIC8vIElmIHRoZSBjb2RlIHNheXMgd2hhdCBsYW5ndWFnZSBpdCBpcywgaGlnaGxpZ2h0IGl0IGluIHRoYXQgbGFuZ3VhZ2VcbiAgICAgICAgICAgIC8vIFdlIGRvbid0IHVzZSBoaWdobGlnaHRFbGVtZW50IGhlcmUgYmVjYXVzZSB3ZSBjYW4ndCBmb3JjZSBsYW5ndWFnZSBkZXRlY3Rpb25cbiAgICAgICAgICAgIC8vIG9mZi4gSXQgc2hvdWxkIHVzZSB0aGUgb25lIHdlJ3ZlIGZvdW5kIGluIHRoZSBDU1MgY2xhc3MgYnV0IHdlJ2QgcmF0aGVyIHBhc3NcbiAgICAgICAgICAgIC8vIGl0IGluIGV4cGxpY2l0bHkgdG8gbWFrZSBzdXJlLlxuICAgICAgICAgICAgY29kZS5pbm5lckhUTUwgPSBoaWdobGlnaHQuaGlnaGxpZ2h0KGNvZGUudGV4dENvbnRlbnQgPz8gXCJcIiwgeyBsYW5ndWFnZTogYWR2ZXJ0aXNlZExhbmcgfSkudmFsdWU7XG4gICAgICAgIH0gZWxzZSBpZiAoXG4gICAgICAgICAgICBTZXR0aW5nc1N0b3JlLmdldFZhbHVlKFwiZW5hYmxlU3ludGF4SGlnaGxpZ2h0TGFuZ3VhZ2VEZXRlY3Rpb25cIikgJiZcbiAgICAgICAgICAgIGNvZGUucGFyZW50RWxlbWVudCBpbnN0YW5jZW9mIEhUTUxQcmVFbGVtZW50XG4gICAgICAgICkge1xuICAgICAgICAgICAgLy8gVXNlciBoYXMgbGFuZ3VhZ2UgZGV0ZWN0aW9uIGVuYWJsZWQgYW5kIHRoZSBjb2RlIGlzIHdpdGhpbiBhIHByZVxuICAgICAgICAgICAgLy8gd2Ugb25seSBhdXRvLWhpZ2hsaWdodCBpZiB0aGUgY29kZSBibG9jayBpcyBpbiBhIHByZSksIHNvIGhpZ2hsaWdodFxuICAgICAgICAgICAgLy8gdGhlIGJsb2NrIHdpdGggYXV0by1oaWdobGlnaHRpbmcgZW5hYmxlZC5cbiAgICAgICAgICAgIC8vIFdlIHBhc3MgaGlnaGxpZ2h0anMgdGhlIHRleHQgdG8gaGlnaGxpZ2h0IHJhdGhlciB0aGFuIGxldHRpbmcgaXRcbiAgICAgICAgICAgIC8vIHdvcmsgb24gdGhlIERPTSB3aXRoIGhpZ2hsaWdodEVsZW1lbnQgYmVjYXVzZSB0aGF0IGFsc28gYWRkcyBDU1NcbiAgICAgICAgICAgIC8vIGNsYXNzZXMgdG8gdGhl