From a0dc46dc565418fef9a257508a92c788c4ec50af Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 16 May 2020 00:01:06 -0400
Subject: fixed some issues with frame animation.  added comic rendering mode. 
 added server certficate keys to get SSL to work?

---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 5e33e7e70..a00eb366b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -206,6 +206,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                     if (json !== curLayout?.Data) {
                         !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize));
+                        !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily));
                         this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
                         this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
                     }
@@ -1230,7 +1231,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     style={{
                         width: "100%",
                         height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
-                        background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
+                        background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
                         opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                         color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
                         pointerEvents: interactive ? "none" : undefined,
-- 
cgit v1.2.3-70-g09d2


From 0460e38b8d1136983c7f36ae1cdf466296345065 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 20 May 2020 15:30:18 -0400
Subject: fixed text boxes to not set height when viewed at renderDepth=0. 
 fixed webBoxt to restore annotations correctly.

---
 src/client/views/nodes/WebBox.tsx                         | 2 ++
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 6 +++---
 2 files changed, 5 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 12166147d..c73b88ef7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -144,6 +144,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null);
         if (future.length) {
             history.push(this._url);
+            this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
             this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!));
             this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)]));
         }
@@ -156,6 +157,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         if (history.length) {
             if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]);
             else future.push(this._url);
+            this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
             this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!));
             this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)]));
         }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a00eb366b..b8fbe3420 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1179,7 +1179,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     @action
     tryUpdateHeight(limitHeight?: number) {
         let scrollHeight = this._ref.current?.scrollHeight;
-        if (this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+        if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1);
             if (limitHeight && scrollHeight > limitHeight) {
                 scrollHeight = limitHeight;
@@ -1224,13 +1224,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 transform: `scale(${scale})`,
                 transformOrigin: "top left",
                 width: `${100 / scale}%`,
-                height: `${100 / scale}%`,
+                height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`,
                 ...this.styleFromLayoutString(scale)
             }}>
                 <div className={`formattedTextBox-cont`} ref={this._ref}
                     style={{
                         width: "100%",
-                        height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
+                        height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined,
                         background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
                         opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                         color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
-- 
cgit v1.2.3-70-g09d2


From b458b4ef1be93161cf2bda250be109fd6a0a5ffe Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 22 May 2020 21:21:04 -0700
Subject: port extensibility fixes

---
 src/Utils.ts                                       |  2 +-
 src/client/views/Main.tsx                          |  6 +++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/client/views/webcam/WebCamLogic.js             | 11 ++++++----
 src/debug/Repl.tsx                                 |  3 ++-
 src/mobile/ImageUpload.tsx                         |  5 +++--
 src/server/ApiManagers/DownloadManager.ts          |  2 +-
 src/server/DashUploadUtils.ts                      |  9 ++++----
 src/server/remapUrl.ts                             |  3 ++-
 src/server/server_Initialization.ts                | 24 +++++++++++++---------
 src/server/websocket.ts                            |  2 ++
 test/test.ts                                       |  3 ++-
 12 files changed, 45 insertions(+), 27 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index bcb215804..ef5002bec 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -43,7 +43,7 @@ export namespace Utils {
     }
 
     /**
-     * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+     * A convenience method. Prepends the full path (i.e. http://localhost:<port>) to the
      * requested extension
      * @param extension the specified sub-path to append to the window origin
      */
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 17c001971..6878658a8 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,12 +5,16 @@ import * as ReactDOM from 'react-dom';
 import * as React from 'react';
 import { DocServer } from "../DocServer";
 import { AssignAllExtensions } from "../../extensions/General/Extensions";
+import { Networking } from "../Network";
 
 AssignAllExtensions();
 
+export let resolvedPorts: { server: number, socket: number };
+
 (async () => {
     const info = await CurrentUserUtils.loadCurrentUser();
-    DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
+    resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts"));
+    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email);
     await Docs.Prototypes.initialize();
     if (info.id !== "__guest__") {
         // a guest will not have an id registered
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b8fbe3420..fc131cd38 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -869,7 +869,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             }
             const marks = [...node.marks];
             const linkIndex = marks.findIndex(mark => mark.type.name === "link");
-            const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
+            const link = view.state.schema.mark(view.state.schema.marks.link, { href: Utils.prepend(`/doc/${linkId}`), location: "onRight", title: title, docref: true });
             marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
             return node.mark(marks);
         }
diff --git a/src/client/views/webcam/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js
index c847b8656..bd1a58cbe 100644
--- a/src/client/views/webcam/WebCamLogic.js
+++ b/src/client/views/webcam/WebCamLogic.js
@@ -1,5 +1,8 @@
 'use strict';
 import io from "socket.io-client";
+import {
+    resolvedPorts
+} from "../../../server/server_Initialization";
 
 var socket;
 var isChannelReady = false;
@@ -29,7 +32,7 @@ export function initialize(roomName, handlerUI) {
 
     room = roomName;
 
-    socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${4321}`);
+    socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${resolvedPorts.socket}`);
 
     if (room !== '') {
         socket.emit('create or join', room);
@@ -104,9 +107,9 @@ export function initialize(roomName, handlerUI) {
 
 
     navigator.mediaDevices.getUserMedia({
-        audio: true,
-        video: true
-    })
+            audio: true,
+            video: true
+        })
         .then(gotStream)
         .catch(function (e) {
             alert('getUserMedia() error: ' + e.name);
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index d541c8009..be53c0b9b 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -7,6 +7,7 @@ import { makeInterface } from '../fields/Schema';
 import { ObjectField } from '../fields/ObjectField';
 import { RefField } from '../fields/RefField';
 import { DocServer } from '../client/DocServer';
+import { resolvedPorts } from '../client/views/Main';
 
 @observer
 class Repl extends React.Component {
@@ -61,6 +62,6 @@ class Repl extends React.Component {
 }
 
 (async function () {
-    DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl");
+    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "repl");
     ReactDOM.render(<Repl />, document.getElementById("root"));
 })();
\ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 231870531..3007fb02f 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -13,6 +13,7 @@ import { observable } from 'mobx';
 import { Utils } from '../Utils';
 import MobileInterface from './MobileInterface';
 import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
+import { resolvedPorts } from '../server/server_Initialization';
 
 
 
@@ -106,10 +107,10 @@ class Uploader extends React.Component {
 }
 
 
-// DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+// DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "image upload");
 (async () => {
     const info = await CurrentUserUtils.loadCurrentUser();
-    DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + "mobile");
+    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email + "mobile");
     await Docs.Prototypes.initialize();
     if (info.id !== "__guest__") {
         // a guest will not have an id registered
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index 01d2dfcad..c5f3ca717 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -246,7 +246,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera
         if (typeof result === "string") {
             let path: string;
             let matches: RegExpExecArray | null;
-            if ((matches = /\:1050\/files\/images\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+            if ((matches = /\:\d+\/files\/images\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
                 // image already exists on our server
                 path = serverPathToFile(Directory.images, matches[1]);
             } else {
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index c887aa4e6..886515286 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -15,6 +15,7 @@ const parse = require('pdf-parse');
 import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager';
 import { red } from 'colors';
 import { Stream } from 'stream';
+import { resolvedPorts } from './server_Initialization';
 const requestImageSize = require("../client/util/request-image-size");
 
 export enum SizeSuffix {
@@ -184,7 +185,7 @@ export namespace DashUploadUtils {
             if (error !== null) {
                 return error;
             }
-            source = `http://localhost:1050${clientPathToFile(Directory.images, resolved)}`;
+            source = `http://localhost:${resolvedPorts.server}${clientPathToFile(Directory.images, resolved)}`;
         }
         let resolvedUrl: string;
         /**
@@ -194,14 +195,14 @@ export namespace DashUploadUtils {
          * basename subtree (i.e. /images/<some_guid>.<ext>) and put it on the end of the server's url.
          * 
          * This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client)
-         * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:1050 refers to the same thing
-         * as the full dash-release.eastus.cloudapp.azure.com:1050.
+         * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:<port> refers to the same thing
+         * as the full dash-release.eastus.cloudapp.azure.com:<port>.
          */
         const matches = isLocal().exec(source);
         if (matches === null) {
             resolvedUrl = source;
         } else {
-            resolvedUrl = `http://localhost:1050/${matches[1].split("\\").join("/")}`;
+            resolvedUrl = `http://localhost:${resolvedPorts.server}/${matches[1].split("\\").join("/")}`;
         }
         // See header comments: not all image files have exif data (I believe only JPG is the only format that can have it)
         const exifData = await parseExifData(resolvedUrl);
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
index 91a3cb6bf..7178add93 100644
--- a/src/server/remapUrl.ts
+++ b/src/server/remapUrl.ts
@@ -1,4 +1,5 @@
 import { Database } from "./database";
+import { resolvedPorts } from "./server_Initialization";
 
 //npx ts-node src/server/remapUrl.ts
 
@@ -34,7 +35,7 @@ async function update() {
                 if (url.href.includes("localhost") && url.href.includes("Bill")) {
                     dynfield = true;
 
-                    update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:1050${url.pathname}` };
+                    update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:${resolvedPorts.server}${url.pathname}` };
                 }
             }
         }
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index d370385b2..8f4d17f22 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -20,8 +20,8 @@ import * as fs from 'fs';
 import * as request from 'request';
 import RouteSubscriber from './RouteSubscriber';
 import { publicDirectory } from '.';
-import { logPort, pathFromRoot, } from './ActionUtilities';
-import { blue, yellow, red } from 'colors';
+import { logPort } from './ActionUtilities';
+import { blue, yellow } from 'colors';
 import * as cors from "cors";
 import { createServer, Server as HttpsServer } from "https";
 import { Server as HttpServer } from "http";
@@ -32,6 +32,8 @@ import { SSL } from './apis/google/CredentialsLoader';
 export type RouteSetter = (server: RouteManager) => void;
 export let disconnect: Function;
 
+export let resolvedPorts: { server: number, socket: number };
+
 export default async function InitializeServer(routeSetter: RouteSetter) {
     const app = buildWithMiddleware(express());
 
@@ -56,16 +58,18 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
 
     let server: HttpServer | HttpsServer;
     const { serverPort } = process.env;
-    const resolved = isRelease && serverPort ? Number(serverPort) : 1050;
+    resolvedPorts.server = isRelease && serverPort ? Number(serverPort) : 1050;
     await new Promise<void>(resolve => server = isRelease ?
-        createServer(SSL.Credentials, app).listen(resolved, resolve) :
-        app.listen(resolved, resolve)
+        createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) :
+        app.listen(resolvedPorts, resolve)
     );
-    logPort("server", resolved);
+    logPort("server", resolvedPorts.server);
 
     // initialize the web socket (bidirectional communication: if a user changes
     // a field on one client, that change must be broadcast to all other clients)
-    WebSocket.initialize(isRelease, app);
+    resolvedPorts.socket = await WebSocket.initialize(isRelease, app);
+
+    app.get("/resolvedPorts", (_req, res) => res.send(resolvedPorts));
 
     disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
     return isRelease;
@@ -174,11 +178,11 @@ function registerRelativePath(server: express.Express) {
     server.use("*", (req, res) => {
         const relativeUrl = req.originalUrl;
         if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference.  So construct a proxied absolute reference here.
-            const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:1050/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
-            const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:1050/corsProxy/ )
+            const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
+            const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ )
             const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart)
             const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g.,  https://en.wikipedia.org)
-            const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:1050/corsProxy/https://en.wikipedia.org/<somethingelse>)
+            const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
             res.redirect(redirectedProxiedUrl);
         } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine
             res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl));
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 21e772e83..b99a463d4 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -137,6 +137,8 @@ export namespace WebSocket {
                 socket.disconnect(true);
             };
         });
+
+        return resolved;
     }
 
     function processGesturePoints(socket: Socket, content: GestureContent) {
diff --git a/test/test.ts b/test/test.ts
index 5fc156b46..068cc2bb9 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -2,7 +2,7 @@ import { expect } from 'chai';
 import 'mocha';
 const { JSDOM } = require('jsdom');
 const dom = new JSDOM("", {
-    url: "http://localhost:1050"
+    url: `http://localhost:${resolvedPorts.server}`
 });
 (global as any).window = dom.window;
 
@@ -12,6 +12,7 @@ import { Doc } from '../src/fields/Doc';
 import { Cast } from '../src/fields/Types';
 import { createSchema, makeInterface, defaultSpec } from '../src/fields/Schema';
 import { ImageField } from '../src/fields/URLField';
+import { resolvedPorts } from '../src/server/server_Initialization';
 describe("Document", () => {
     it('should hold fields', () => {
         const key = "Test";
-- 
cgit v1.2.3-70-g09d2

