diff --git a/README.md b/README.md index 68e6fc3..7f239e6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,31 @@ draw.io integration in phorge ============================= Installation ============ 1) Extract the content of this repository into /src/extensions 2) /bin/arc liberate 3) /bin/storage upgrade 4) CD to /src/extensions/drawio/data 5) git clone https://github.com/jgraph/drawio.git 6) Diagrams application is available under "More Applications" in Phorge. You may add it to your navigator menu via "Edit Menu" -You may need to set the following rule in your httpd.conf for your Phorge's VirtualHost: - - Header set Content-Security-Policy "default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline';" - - Usage ===== When you start the Diagrams application, you will see the embedded version of [draw.io](https://app.diagrams.net/). You can reference the diagrams in your wiki pages or maniphest tasks by means of the DIAG token. E.g. {DIAG123} When you doubleclick on a diagram, the editor will open the corresponding diagram in new browser tab. You can create diagrams with multiple pages, but only the first one will be visualized. When you modify an existing diagram, a new version will be created. You can select older versions in the editor by means of the dropdown in the topright corner. If you don't see a dropdown, your diagram has only 1 version. Extra info ========== https://github.com/jgraph/drawio-integration #diagrams_extension diff --git a/data/iframe-toolbtn.css b/data/iframe-toolbtn.css new file mode 100644 index 0000000..06ba82f --- /dev/null +++ b/data/iframe-toolbtn.css @@ -0,0 +1,113 @@ +/* begin stylesheets for version toolbutton */ +.toolbtn-version-dropdown { + right: 0; + white-space: nowrap; +} + +.toolbtn-version-hasversions .btnSave { + margin-right: 8px; +} + +.toolbtn-version-dropdown { + display: none; + font-family: -apple-system, BlinkMacSystemFont, + "Segoe UI Variable", "Segoe UI", system-ui, + ui-sans-serif, Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; +} + +.toolbtn-version-hasversions .toolbtn-version-dropdown { + display: block; +} + +.toolbtn-version-dropdown-toggle { + background: #eee; + border: 1px solid #d8d8d8; + border-radius: 4px; + margin-left: 8px; + margin-right: 4px; + padding: 6px; +} + +.toolbtn-version-dropdown.open .toolbtn-version-dropdown-toggle { + box-shadow: inset 1px 1px 3px #0008; +} + +.toolbtn-version-dropdown-toggle:hover:not([disabled]) { + background: #e5e5e5; +} + +.toolbtn-version-dropdown-menu { + display: none; + position: absolute; + top: 40px; + right: 0; + left: auto; + margin-top: -2px; + padding: 10px; + border: solid 1px #444; + background: #eee; + border-radius: 5px 0px 0px 5px; +} + +.toolbtn-version-dropdown-menu a { + color: #000; +} + +.toolbtn-version-dropdown-menu.open { + display: block; + z-index: 4; +} + +.toolbtn-version-dropdown-menu .menu-item { + padding: 2px; +} + +.toolbtn-version-dropdown-menu .menu-item a { + line-height: 20px; +} + +.toolbtn-version-dropdown-menu .menu-item:hover { + background: #29b6f2; + color: #fff; + margin-left: -2px; + margin-right: 2px; +} + +.toolbtn-version-dropdown-menu .menu-item:hover a { + color: #fff; +} + +.toolbtn-version-list { + list-style: none; + margin: 0; + padding: 0; + margin-bottom: 8px; +} +/* end stylesheets for version toolbutton */ + +/* begin stylesheets for subscription toolbutton */ +.toolbtn-diagram-subscription button { + display: none; +} + +.toolbtn-diagram-subscription.unsubscribed button.subscribe, +.toolbtn-diagram-subscription.subscribed button.unsubscribe { + display: inline-block; +} + +.geEditor .toolbtn-diagram-subscription button.eye, +.geEditor .toolbtn-diagram-subscription button:hover:not([disabled]) { + background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAADWUlEQVRYhe2V3yu7bxjHX88zs6emlBOfshoTLZMVOVmZpFaUM3HgRIqUf8ABzfdEOUEKB0I5c7CmHAhbjuxAidQOlpnQoqgR82y2Pff3wPe7+Gx+HXyOPnsf3s913e/Xfd/PdV1QUEEF/e2SfhJstVrFy8vLt2Krqqrw+/1f7v8tgJqaGpHJZN6t1dbW0tPTQ2VlJfPz8xwdHeXNra6uZnd390Mf+TPj9vZ2YbFYRCaTwWKxZNf7+vrY2Nigq6uLoqIihBA5uQaDgYqKCs7OzqipqckN+AqgsbFRnJ+fU1lZicfj4devXwA4HA5GR0fZ3NzE5XLR39/P8fFxTn4ymWRycpKhoSH+O0BeiLwAdrtd3N/f43Q68Xg8lJaWEggEABgcHCQcDjMxMcHDw8NH/ABsb28zMjLCwsICiqLkhcgBcDgc4vHxkZaWFubm5kgkEkSj0ez36upq9vf3UVX1U3OA6+trnp6eaG5uZnFxEb1enwORA3Bzc4PVamV2dpZYLAbA2x/Q6/Wys7PzpTmApmlIkoSqqtTX1zM1NQW8VlNeAIvFIhRFYXp6mkQiQXFxMUKI7PsDzMzMcHJy8i0Ak8mUhU8kErS0tNDd3c3bUs65AUmSeHx8JJlMZk9QUlKC3W7/lulbOZ1O0uk0kvRahalUit/L+R1AJBKRVFVlbGwsG/h/8vDw8I/MGxoasNvt6HQ6ZFlGp9OxtbWF1+uluLg4G6f7PdFms01cXFwQCoVoa2tDll8ZzWYzqqp+6/pLS0tZWlpCr9dTVFSEpmkcHBzgdrvRNI3T09NsY8oBuLy8/MdsNk9EIhGCwSBOpxODwYAQgtbWVjRN4/Dw8ENzs9nM6uoqZWVlJJNJJElib2+P8fFxUqkUkUjkXVf8sEU2NTWJWCyGyWTC7XZTV1eHEAKj0cjV1RVra2sEAgFub29RFIXa2lo6Ozvp7e0lHo+TSqV4fn5meXmZ9fV1gBzzTwEAXC6XCIfDyLJMR0cHAwMDlJWVoSgKJSUlGAwGdDodQgjS6TSqqhKPx8lkMvh8PlZWVohGo8iyTDgczuv1o2EkyzKNjY04nU5sNhvl5eUYjUY0TePu7o5QKEQwGMTv93N3dwd8PYz+2Di2WCz4fL4f7V9QQQX9nfoXrlRoxrjjl+oAAAAASUVORK5CYII=); + width:36px; + height:32px; +} + +.geEditor .toolbtn-diagram-subscription button.unsubscribe:hover:not([disabled]) { + opacity: .75; +} + +.toolbtn-diagram-subscription.subscribed button.eye { + filter:invert(100%) opacity(50%); +} +/* end stylesheets for subscription toolbutton */ diff --git a/data/iframe-toolbtn.js b/data/iframe-toolbtn.js new file mode 100644 index 0000000..b56c27b --- /dev/null +++ b/data/iframe-toolbtn.js @@ -0,0 +1,251 @@ +/** + * global dicttionary containing all configuration of all installed toolbuttons + */ +const toolbtn = {}; + +/** + * Abstract parent class for creating ToolButtons + */ +class ToolButton { + constructor(name) { + if (new.target === ToolButton) { + throw new TypeError("Cannot construct ToolButton instances directly"); + } + + toolbtn[name] = {}; + toolbtn[name].instance = this; + + parent.phorge_extension.toolbtn = toolbtn; + } + + initialize() { + throw new Error("Method 'initialize' must be implemented by " + + this.constructor.name); + } +} + +/** + * ToolButton class which shows a dropdown menu, containing all the available + * diagram versions + */ +class ToolButtonVersion extends ToolButton { + constructor(name) { + super(name); + this.currentPage = 1; + this.itemsPerPage = 5; + } + + initialize() { + toolbtn.version.dropdown = document.querySelector('.toolbtn-version-dropdown'); + toolbtn.version.toggle = document.querySelector('.toolbtn-version-dropdown-toggle'); + toolbtn.version.menu = document.querySelector('.toolbtn-version-dropdown-menu'); + toolbtn.version.prevBtn = document.querySelector('.toolbtn-version-prev-btn'); + toolbtn.version.nextBtn = document.querySelector('.toolbtn-version-next-btn'); + + if (phorge_extension.diagramName != "") { + // update the version list when the page loads + this.requestVersionInfo(1); + } + + toolbtn.version.toggle.addEventListener('click', () => { + toolbtn.version.dropdown.classList.toggle('open'); + toolbtn.version.menu.classList.toggle('open'); + }); + + toolbtn.version.prevBtn.addEventListener('click', (event) => { + event.stopPropagation(); + + var versionList = document.querySelector('.toolbtn-version-list'); + var pagecount = parseInt(versionList.dataset.pagecount); + + if (this.currentPage > 1) { + this.currentPage--; + this.requestVersionInfo(this.currentPage); + } + + toolbtn.version.prevBtn.disabled = (this.currentPage <= 1); + toolbtn.version.nextBtn.disabled = (this.currentPage >= pagecount); + }); + + toolbtn.version.nextBtn.addEventListener('click', (event) => { + event.stopPropagation(); + + var versionList = document.querySelector('.toolbtn-version-list'); + var pagecount = parseInt(versionList.dataset.pagecount); + + if (this.currentPage <= pagecount) { + this.currentPage++; + this.requestVersionInfo(this.currentPage); + } + + toolbtn.version.prevBtn.disabled = (this.currentPage <= 1); + toolbtn.version.nextBtn.disabled = (this.currentPage >= pagecount); + }); + + document.addEventListener('click', (event) => { + if (!toolbtn.version.dropdown.contains(event.target)) { + toolbtn.version.dropdown.classList.remove('open'); + toolbtn.version.menu.classList.remove('open'); + } + }); + } + + requestVersionInfo(page) { + var xmlhttp = new XMLHttpRequest(); + var url = phorge_extension.baseURI + + '/version/' + + phorge_extension.diagramName + + '/' + + page; + + xmlhttp.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + try { + var json = JSON.parse(this.responseText); + if (page == 1 && json.data.length > 1) { + document.body.classList.add('toolbtn-version-hasversions'); + } + + // overwrite version info dropdown + var versionList = document.querySelector('.toolbtn-version-list'); + versionList.innerHTML = ''; + json.data.forEach(function(v) { + var anchor = document.createElement('a'); + anchor.innerText = '#' + + v.id + + ': ' + + v.datetime + + ' (' + + v.author + + ')'; + + var versionedUrl = phorge_extension.baseURI + '/' + + phorge_extension.diagramName ; + if (page != 1 || v.id != json.data[0].id) { + // do not add version to anchor if we have the latest version + versionedUrl += '/' + v.id; + } + + anchor.addEventListener('click', function() { + parent.location.href = versionedUrl; + }); + + var listitem = document.createElement('li'); + listitem.classList.add('menu-item'); + listitem.appendChild(anchor); + versionList.appendChild(listitem); + + versionList.dataset.pagecount = json.pagecount; + + if (json.nopager) { + toolbtn.version.prevBtn.style.display = "none"; + toolbtn.version.nextBtn.style.display = "none"; + } else { + toolbtn.version.prevBtn.style.display = "inline-block"; + toolbtn.version.nextBtn.style.display = "inline-block"; + } + }); + } + catch { + } + } + }; + xmlhttp.open("GET", url, true); + xmlhttp.send(); + } +} + +/** + * ToolButton class which shows a button with an eye in it. + * By clicking on it, you can subscribe or unsubscribe to changes on the + * current diagram. + * When the eye is inverted drawn, the current viewer is subscribed to + * the current diagram + */ +class ToolButtonSubscription extends ToolButton { + constructor(name) { + super(name); + } + + initialize() { + toolbtn.subscription.btnSubscriptions = document.querySelector('div.toolbtn-diagram-subscription'); + toolbtn.subscription.btnSubscribe = document.querySelector('.toolbtn-diagram-subscription button.subscribe'); + toolbtn.subscription.btnUnsubscribe = document.querySelector('.toolbtn-diagram-subscription button.unsubscribe'); + + toolbtn.subscription.btnSubscribe.addEventListener('click', (event) => { + event.stopPropagation(); + this.subscription('add'); + }); + + toolbtn.subscription.btnUnsubscribe.addEventListener('click', (event) => { + event.stopPropagation(); + this.subscription('delete'); + }); + + if (phorge_extension.diagramPHID !== "") { + this.getSubscriptionState(); + } + } + + getSubscriptionState() { + var csrf = phorge_extension.csrf; + var data = new URLSearchParams(); + data.append('__csrf__', csrf); + data.append('__form__', '1'); + data.append('__ajax__', 'true'); + + // send second AJAX call to verify if subscription was executed + var responseSubscription = new XMLHttpRequest(); + responseSubscription.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + var result = JSON.parse(responseSubscription.responseText); + + toolbtn.subscription.btnSubscriptions.classList.remove('subscribed'); + toolbtn.subscription.btnSubscriptions.classList.remove('unsubscribed'); + + if (result.subscribed == true) { + toolbtn.subscription.btnSubscriptions.classList.add('subscribed'); + } else { + toolbtn.subscription.btnSubscriptions.classList.add('unsubscribed'); + } + } + }; + + var url = phorge_extension.baseURI + + '/subscribed/request/' + + phorge_extension.diagramPHID + + '/'; + + responseSubscription.overrideMimeType("application/json"); + responseSubscription.open('POST', url, true); + responseSubscription.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); + responseSubscription.send(data); + } + + subscription(addDelete) { + var self = this; + var csrf = phorge_extension.csrf; + var data = new URLSearchParams(); + var phid = phorge_extension.diagramPHID; + data.append('__csrf__', csrf); + data.append('__form__', '1'); + data.append('__ajax__', 'true'); + + var requestSubscription = new XMLHttpRequest(); + requestSubscription.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + self.getSubscriptionState(); + } + }; + requestSubscription.overrideMimeType("application/json"); + requestSubscription.open('POST', "/subscriptions/" + addDelete + "/" + phid + "/", true); + requestSubscription.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); + requestSubscription.send(data); + } +} + +/** + * Initialization of all ToolButton classes + */ +new ToolButtonVersion('version'); +new ToolButtonSubscription('subscription'); \ No newline at end of file diff --git a/data/iframe.css b/data/iframe.css new file mode 100644 index 0000000..3a7bdfe --- /dev/null +++ b/data/iframe.css @@ -0,0 +1,97 @@ +body { overflow:hidden; } +div.picker { z-index: 10007; } +.geSidebarContainer .geTitle input { + font-size:8pt; + color:#606060; +} +.geBlock { + z-index:-3; + margin:100px; + margin-top:40px; + margin-bottom:30px; + padding:20px; + text-align:center; + min-width:50%; +} +.geBlock h1, .geBlock h2 { + margin-top:0px; + padding-top:0px; +} +.geEditor *:not(.geScrollable)::-webkit-scrollbar { + width:10px; + height:10px; +} +.geEditor ::-webkit-scrollbar-track { + background-clip:padding-box; + border:solid transparent; + border-width:1px; +} +.geEditor ::-webkit-scrollbar-corner { + background-color:transparent; +} +.geEditor ::-webkit-scrollbar-thumb { + background-color:rgba(0,0,0,.1); + background-clip:padding-box; + border:solid transparent; + border-radius:10px; +} +.geEditor ::-webkit-scrollbar-thumb:hover { + background-color:rgba(0,0,0,.4); +} +.geTemplate { + border:1px solid transparent; + display:inline-block; + _display:inline; + vertical-align:top; + border-radius:3px; + overflow:hidden; + font-size:14pt; + cursor:pointer; + margin:5px; +} + +@keyframes init-spinner-animation { + 0%, 39%, 100% { + opacity: 0.33; + } + 40% { + opacity: .83; + } +} +.init-spinner { + position: relative; + height: 80px; + width: 80px; + left: calc(50% - 50px); +} +.init-spinner .content { + position: absolute; + width: 0px; + left: 50%; + top: 50%; +} + +.init-spinner .content .spike { + position: absolute; + top: -2.5px; + width: 15px; + height: 5px; + background: #000c; + border-radius: 2.5px; + transform-origin: left center 0px; +} + +.init-spinner .content .spike .animator { + width: 100%; + height: 100%; + background: rgb(255, 255, 255); + border-radius: 2.5px; + box-shadow: transparent 0px 0px 1px; + animation-name: init-spinner-animation; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-direction: normal; + animation-fill-mode: none; + animation-play-state: running; +} \ No newline at end of file diff --git a/data/iframe1.js b/data/iframe1.js new file mode 100644 index 0000000..5a3ed22 --- /dev/null +++ b/data/iframe1.js @@ -0,0 +1,144 @@ +var urlParams = (function() { + var result = {}; + var params = window.location.search.slice(1).split('&'); + + for (var i = 0; i < params.length; i++) { + var idx = params[i].indexOf('='); + + if (idx > 0) { + result[params[i].substring(0, idx)] = params[i].substring(idx + 1); + } + } + + return result; +})(); + +if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P') { + try { + urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2))); + + if (urlParams.hash != null) { + window.location.hash = urlParams.hash; + } + } + catch (e) { + } +} + +(function() { + var proto = window.location.protocol; + var host = window.location.host; + var href = proto + '//' + host + window.location.href.substring( + window.location.protocol.length + + window.location.host.length + 2); + + if (href != window.location.href) { + window.location.href = href; + } +})(); + +function mxmeta(name, content, httpEquiv) { + try { + var s = document.createElement('meta'); + + if (name != null) { + s.setAttribute('name', name); + } + + s.setAttribute('content', content); + + if (httpEquiv != null) { + s.setAttribute('http-equiv', httpEquiv); + } + + var t = document.getElementsByTagName('meta')[0]; + t.parentNode.insertBefore(s, t); + } + catch (e) + { + } +} + +function mxscript(src, onLoad, id, dataAppKey, noWrite, onError) { + var s = document.createElement('script'); + s.setAttribute('type', 'text/javascript'); + s.setAttribute('defer', 'true'); + s.setAttribute('src', src); + + if (id != null) { + s.setAttribute('id', id); + } + + if (dataAppKey != null) { + s.setAttribute('data-app-key', dataAppKey); + } + + if (onLoad != null) { + var r = false; + + s.onload = s.onreadystatechange = function() { + if (!r && (!this.readyState || this.readyState == 'complete')) { + r = true; + onLoad(); + } + }; + } + + if (onError != null) { + s.onerror = function(e) { + onError('Failed to load ' + src, e); + }; + } + + var t = document.getElementsByTagName('script')[0]; + + if (t != null) { + t.parentNode.insertBefore(s, t); + } +} + +function mxinclude(src) { + var g = document.createElement('script'); + g.type = 'text/javascript'; + g.async = true; + g.src = src; + + var s = document.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(g, s); +} + +(function() { + var name = 'diagrams.net'; + mxmeta('apple-mobile-web-app-title', name); + mxmeta('application-name', name); +})(); + +var isLocalStorage = true; +var mxScriptsLoaded = false, mxWinLoaded = false; + +function checkAllLoaded() { + if (mxScriptsLoaded && mxWinLoaded) { + App.main(); + } +} + +var t0 = new Date(); + (function() { + function loadAppJS() { + mxscript('js/app.min.js', function() { + mxScriptsLoaded = true; + checkAllLoaded(); + mxscript('js/PostConfig.js'); + }); + } + + mxscript('js/PreConfig.js', loadAppJS); +})(); + +window.onerror = function() { + var status = document.getElementById('geStatus'); + + if (status != null) { + status.innerHTML = 'Page could not be loaded. Please try refreshing.'; + } +}; diff --git a/data/iframe2.js b/data/iframe2.js new file mode 100644 index 0000000..ff355fd --- /dev/null +++ b/data/iframe2.js @@ -0,0 +1,7 @@ +window.addEventListener('load', function() +{ + mxWinLoaded = true; + checkAllLoaded(); +}); + +phorge_extension = parent.phorge_extension; \ No newline at end of file diff --git a/data/phorge_extension.js b/data/phorge_extension.js index 003a2f7..6044413 100644 --- a/data/phorge_extension.js +++ b/data/phorge_extension.js @@ -1,579 +1,246 @@ var phorge_extension = {}; -function copySettingsToIframe() { - var iframe = document.querySelector('iframe'); - const script = document.createElement('script'); - script.textContent = ` - const phorge_extension = JSON.parse('` + JSON.stringify(phorge_extension) + `'); - `; - iframe.contentDocument.body.appendChild(script); - iframe.contentDocument.body.wawa = 1; -} - function edit(image) { var iframe = document.createElement('iframe'); + var iframeInitializedEvent = new CustomEvent('initReceived'); iframe.setAttribute('title', 'diagrams.net editor'); iframe.setAttribute('frameborder', '0'); iframe.style.width = '100%'; iframe.style.height = 'calc(100vh - 91px)'; iframe.style.marginTop = '30px'; iframe.style.marginBottom = '-16px'; image.style.display = 'none'; var receive = function (evt) { if (evt.data.length > 0) { var msg = JSON.parse(evt.data); if (msg.event == 'init') { if (phorge_extension.diagramBase64) { image.src = 'data:image/png;base64,' + phorge_extension.diagramBase64; var diagramName = document.querySelector('.diagramName'); diagramName.style.display = 'inline-block'; diagramName.querySelector('a').innerText = phorge_extension.diagramName; if (phorge_extension.diagramVersion != '') { diagramName.querySelector('.version').innerText = '(#' + phorge_extension.diagramVersion + ')'; } } iframe.contentWindow.postMessage(JSON.stringify({ action: 'load', autosave: 1, xmlpng: image.getAttribute('src') }), '*'); } else if (msg.event == 'load') { // enable Mathematical Typesettings by default iframe.contentWindow.sb.editorUi.setMathEnabled(true); - copySettingsToIframe(); - setupButtonsInMenuToolbar(); - - loadingtext.style.display = 'none'; } else if (msg.event == 'export') { saveFlowchart(name, msg.data, iframe); } - else if (msg.event == 'exit') { - var originURL = sessionStorage['originURL']; - if (originURL) { - sessionStorage.removeItem('originURL'); - document.location.href = originURL; - } - } else if (msg.event == 'save') { iframe.contentWindow.postMessage(JSON.stringify({ action: 'export', format: 'xmlpng', xml: msg.xml, spin: 'Updating page' }), '*'); } } }; window.addEventListener('message', receive); iframe.setAttribute('src', phorge_extension.editor); document.querySelector('#mainScreen').appendChild(iframe); + iframe.dispatchEvent(iframeInitializedEvent); + iframe.contentWindow.RESOURCES_PATH = document.baseURI + 'iframe/resources'; iframe.contentWindow.STENCIL_PATH = document.baseURI + 'iframe/stencils'; iframe.contentWindow.IMAGE_PATH = document.baseURI + 'iframe/images'; iframe.contentWindow.STYLE_PATH = document.baseURI + 'iframe/styles'; iframe.contentWindow.CSS_PATH = document.baseURI + 'iframe/styles'; } function loadJsExtension(diagramName, diagramPHID, diagramVersion, diagramBase64) { var baseURI = document.baseURI; if (diagramName != '' && diagramBase64 != '') { baseURI = baseURI.substr(0, baseURI.length - diagramName.length - 1); if (diagramVersion != '') { baseURI = baseURI.substr(0, baseURI.length - diagramVersion.length - 1); } } phorge_extension.baseURI = baseURI; phorge_extension.csrf = document.querySelector('input[name="__csrf__"]')?.value; - phorge_extension.editor = baseURI + '/iframe/?embed=1&spin=1&proto=json&noExitBtn=1'; + phorge_extension.editor = baseURI + '/iframe/?embed=1&spin=0&proto=json&noExitBtn=1'; phorge_extension.name = null; phorge_extension.editor += '&lang=en'; phorge_extension.editor += '&ui=min'; phorge_extension.diagramPHID = diagramPHID; phorge_extension.diagramName = diagramName; phorge_extension.diagramVersion = diagramVersion; phorge_extension.diagramBase64 = diagramBase64; document.addEventListener('DOMContentLoaded', function () { edit(document.querySelector('img.drawio')); }, false); } function saveFlowchart(name, flowchartData, iframe) { var diagramID = document.querySelector('.diagramName a').innerText.replace(/^DIAG/, ''); var csrf = document.querySelector('input[name="__csrf__"]')?.value; var data = new URLSearchParams(); data.append('data', flowchartData); data.append('diagramID', diagramID); data.append('__csrf__', csrf); data.append('__form__', '1'); data.append('__ajax__', 'true'); var xmlhttp = new XMLHttpRequest(); xmlhttp.overrideMimeType('application/json'); xmlhttp.open('POST', 'save/', true); xmlhttp.onload = function () { if (xmlhttp.readyState == 4) { var errorMessage = null; try { var result = JSON.parse(xmlhttp.responseText); if (result.Status != 'OK') { errorMessage = result.Error; } else { // make sure we don't show messagebox about redirection in browser iframe.parentNode.removeChild(iframe); if (!phorge_extension.diagramVersion || !phorge_extension.diagramVersion.trim()) { if (!phorge_extension.diagramName || !phorge_extension.diagramName.trim()) { // load new diagram window.location = window.location + '/DIAG' + result.DiagramID; } else { // reload actual page (so versioned diagrams info is also updated) window.location.reload(); } } else { // cut off version id from url var url = document.baseURI .substring(0, document.baseURI .length - phorge_extension.diagramVersion .length - 1 ); // redirect to latest version of diagram window.location = url; } } } catch (exc) { errorMessage = exc.message; } } }; xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); xmlhttp.send(data); } function setupButtonsInMenuToolbar() { var iframe = document.querySelector('iframe'); var btnSave = Array.prototype.slice.call( iframe.contentDocument .querySelector('.geMenubarContainer') .querySelectorAll('button'), 0 ).reverse()[0]; // identify Exit Button btnSave.classList.add('btnSave'); // change layout settings of area where btnSave belongs to so // that the dropdown menu is not hidden under the drawing area btnSave.parentNode.style.position = 'fixed'; // create extra controls var subscribeUnsubscribe = setupSubscriptionButtonInMenuToolbar(iframe, btnSave); var dropdown = setupVersionDropDownInMenuToolbar(iframe, subscribeUnsubscribe); + + // initialize toolbuttons + for (var tb in phorge_extension.toolbtn) { + if (phorge_extension.toolbtn.hasOwnProperty(tb)) { + phorge_extension.toolbtn[tb].instance.initialize(); + } + } } function setupVersionDropDownInMenuToolbar(iframe, btnLeft) { // generate 'Select Version' button // create the dropdown element const dropdown = document.createElement('div'); - dropdown.classList.add('dropdown'); + dropdown.classList.add('toolbtn-version-dropdown'); // create the dropdown toggle button const toggle = document.createElement('button'); - toggle.classList.add('dropdown-toggle'); + toggle.classList.add('toolbtn-version-dropdown-toggle'); toggle.textContent = 'Select Version'; toggle.title = 'View previous versions'; dropdown.appendChild(toggle); const iframeContent = document.querySelector('iframe').contentDocument; const menubarContainer = iframeContent.querySelector('.geMenubarContainer'); // create the dropdown menu const menu = document.createElement('div'); - menu.classList.add('dropdown-menu'); + menu.classList.add('toolbtn-version-dropdown-menu'); menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); // create the version list const versionList = document.createElement('ul'); - versionList.classList.add('version-list'); + versionList.classList.add('toolbtn-version-list'); menu.appendChild(versionList); // create the prev and next buttons const prevBtn = document.createElement('button'); - prevBtn.classList.add('prev-btn'); + prevBtn.classList.add('toolbtn-version-prev-btn'); prevBtn.textContent = '<'; const nextBtn = document.createElement('button'); - nextBtn.classList.add('next-btn'); + nextBtn.classList.add('toolbtn-version-next-btn'); nextBtn.textContent = '>'; menu.appendChild(prevBtn); menu.appendChild(nextBtn); // place dropdown next to button on the left btnLeft.parentNode.insertBefore(dropdown, btnLeft.nextSibling); - // add the corresponding CSS - const style = document.createElement('style'); - style.textContent = ` - .dropdown { - right: 0; - white-space: nowrap; - } - - .hasversions .btnSave { - margin-right: 8px; - } - - .dropdown { - display: none; - font-family: -apple-system, BlinkMacSystemFont, - "Segoe UI Variable", "Segoe UI", system-ui, - ui-sans-serif, Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji"; - } - - .hasversions .dropdown { - display: block; - } - - .dropdown-toggle { - background: #eee; - border: 1px solid #d8d8d8; - border-radius: 4px; - margin-left: 8px; - margin-right: 4px; - padding: 6px; - } - - .dropdown.open .dropdown-toggle { - box-shadow: inset 1px 1px 3px #0008; - } - - .dropdown-toggle:hover:not([disabled]) { - background: #e5e5e5; - } - - .dropdown-menu { - display: none; - position: absolute; - top: 40px; - right: 0; - left: auto; - margin-top: -2px; - padding: 10px; - border: solid 1px #444; - background: #eee; - border-radius: 5px 0px 0px 5px; - } - - .dropdown-menu a { - color: #000; - } - - .dropdown-menu.open { - display: block; - z-index: 4; - } - - .dropdown-menu .menu-item { - padding: 2px; - } - - .dropdown-menu .menu-item a { - line-height: 20px; - } - - .dropdown-menu .menu-item:hover { - background: #29b6f2; - color: #fff; - margin-left: -2px; - margin-right: 2px; - } - - .dropdown-menu .menu-item:hover a { - color: #fff; - } - - .version-list { - list-style: none; - margin: 0; - padding: 0; - margin-bottom: 8px; - } -`; - iframe.contentDocument.head.appendChild(style); - - // add the corresponding javascript - const script = document.createElement('script'); - script.textContent = ` - const dropdown = document.querySelector('.dropdown'); - const toggle = document.querySelector('.dropdown-toggle'); - const menu = document.querySelector('.dropdown-menu'); - const prevBtn = document.querySelector('.prev-btn'); - const nextBtn = document.querySelector('.next-btn'); - const versionList = document.querySelector('.version-list'); - - let currentPage = 1; - const itemsPerPage = 5; - - function goToPhorgeUrl(url) { - parent.location.href = url; - } - - function requestVersionInfo(page) { - var xmlhttp = new XMLHttpRequest(); - var url = phorge_extension.baseURI - + '/version/' - + phorge_extension.diagramName - + '/' - + page; - - xmlhttp.onreadystatechange = function () { - if (this.readyState == 4 && this.status == 200) { - try { - var json = JSON.parse(this.responseText); - if (page == 1 && json.data.length > 1) { - document.body.classList.add('hasversions'); - } - - // overwrite version info dropdown - var versionList = document.querySelector('.version-list'); - versionList.innerHTML = ''; - json.data.forEach(function(v) { - var anchor = document.createElement('a'); - anchor.innerText = '#' - + v.id - + ': ' - + v.datetime - + ' (' - + v.author - + ')'; - - var versionedUrl = phorge_extension.baseURI + '/' - + phorge_extension.diagramName ; - if (page != 1 || v.id != json.data[0].id) { - // do not add version to anchor if we have the latest version - versionedUrl += '/' + v.id; - } - - anchor.href = 'javascript:goToPhorgeUrl("' + versionedUrl + '")'; - var listitem = document.createElement('li'); - listitem.classList.add('menu-item') - listitem.appendChild(anchor); - versionList.appendChild(listitem); - - versionList.dataset.pagecount = json.pagecount; - - if (json.nopager) { - prevBtn.style.display = "none"; - nextBtn.style.display = "none"; - } else { - prevBtn.style.display = "inline-block"; - nextBtn.style.display = "inline-block"; - } - }) - } - catch { - } - } - }; - xmlhttp.open("GET", url, true); - xmlhttp.send(); - } - - if (phorge_extension.diagramName != "") { - // update the version list when the page loads - requestVersionInfo(1); - } - - toggle.addEventListener('click', () => { - dropdown.classList.toggle('open'); - menu.classList.toggle('open'); - }); - - prevBtn.addEventListener('click', (event) => { - event.stopPropagation(); - - var versionList = document.querySelector('.version-list'); - var pagecount = parseInt(versionList.dataset.pagecount); - - if (currentPage > 1) { - currentPage--; - requestVersionInfo(currentPage); - } - - prevBtn.disabled = (currentPage <= 1); - nextBtn.disabled = (currentPage >= pagecount); - }); - - nextBtn.addEventListener('click', (event) => { - event.stopPropagation(); - - var versionList = document.querySelector('.version-list'); - var pagecount = parseInt(versionList.dataset.pagecount); - - if (currentPage <= pagecount) { - currentPage++; - requestVersionInfo(currentPage); - } - - prevBtn.disabled = (currentPage <= 1); - nextBtn.disabled = (currentPage >= pagecount); - }); - - document.addEventListener('click', (event) => { - if (!dropdown.contains(event.target)) { - dropdown.classList.remove('open'); - menu.classList.remove('open'); - } - }); - `; - iframe.contentDocument.body.appendChild(script); - return dropdown; } function setupSubscriptionButtonInMenuToolbar(iframe, btnLeft) { // generate 'Subscribe/Unsubscribe' button // create grouping div element const div = document.createElement('div'); - div.classList.add('diagram-subscription'); + div.classList.add('toolbtn-diagram-subscription'); // create subscribe button const btnSubscribe = document.createElement('button'); btnSubscribe.classList.add('subscribe'); btnSubscribe.classList.add('eye'); btnSubscribe.title = 'Subscribe'; div.appendChild(btnSubscribe); // create unsubscribe button const btnUnsubscribe = document.createElement('button'); btnUnsubscribe.classList.add('unsubscribe'); btnUnsubscribe.classList.add('eye'); btnUnsubscribe.title = 'Unsubscribe'; div.appendChild(btnUnsubscribe); // place grouping next to button on the left btnLeft.parentNode.insertBefore(div, btnLeft.nextSibling); - // add the corresponding CSS - const style = document.createElement('style'); - style.textContent = ` - .diagram-subscription button { - display: none; - } - - .diagram-subscription.unsubscribed button.subscribe, - .diagram-subscription.subscribed button.unsubscribe { - display: inline-block; - } - - .diagram-subscription button.eye, - .diagram-subscription button:hover:not([disabled]) { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAADWUlEQVRYhe2V3yu7bxjHX88zs6emlBOfshoTLZMVOVmZpFaUM3HgRIqUf8ABzfdEOUEKB0I5c7CmHAhbjuxAidQOlpnQoqgR82y2Pff3wPe7+Gx+HXyOPnsf3s913e/Xfd/PdV1QUEEF/e2SfhJstVrFy8vLt2Krqqrw+/1f7v8tgJqaGpHJZN6t1dbW0tPTQ2VlJfPz8xwdHeXNra6uZnd390Mf+TPj9vZ2YbFYRCaTwWKxZNf7+vrY2Nigq6uLoqIihBA5uQaDgYqKCs7OzqipqckN+AqgsbFRnJ+fU1lZicfj4devXwA4HA5GR0fZ3NzE5XLR39/P8fFxTn4ymWRycpKhoSH+O0BeiLwAdrtd3N/f43Q68Xg8lJaWEggEABgcHCQcDjMxMcHDw8NH/ABsb28zMjLCwsICiqLkhcgBcDgc4vHxkZaWFubm5kgkEkSj0ez36upq9vf3UVX1U3OA6+trnp6eaG5uZnFxEb1enwORA3Bzc4PVamV2dpZYLAbA2x/Q6/Wys7PzpTmApmlIkoSqqtTX1zM1NQW8VlNeAIvFIhRFYXp6mkQiQXFxMUKI7PsDzMzMcHJy8i0Ak8mUhU8kErS0tNDd3c3bUs65AUmSeHx8JJlMZk9QUlKC3W7/lulbOZ1O0uk0kvRahalUit/L+R1AJBKRVFVlbGwsG/h/8vDw8I/MGxoasNvt6HQ6ZFlGp9OxtbWF1+uluLg4G6f7PdFms01cXFwQCoVoa2tDll8ZzWYzqqp+6/pLS0tZWlpCr9dTVFSEpmkcHBzgdrvRNI3T09NsY8oBuLy8/MdsNk9EIhGCwSBOpxODwYAQgtbWVjRN4/Dw8ENzs9nM6uoqZWVlJJNJJElib2+P8fFxUqkUkUjkXVf8sEU2NTWJWCyGyWTC7XZTV1eHEAKj0cjV1RVra2sEAgFub29RFIXa2lo6Ozvp7e0lHo+TSqV4fn5meXmZ9fV1gBzzTwEAXC6XCIfDyLJMR0cHAwMDlJWVoSgKJSUlGAwGdDodQgjS6TSqqhKPx8lkMvh8PlZWVohGo8iyTDgczuv1o2EkyzKNjY04nU5sNhvl5eUYjUY0TePu7o5QKEQwGMTv93N3dwd8PYz+2Di2WCz4fL4f7V9QQQX9nfoXrlRoxrjjl+oAAAAASUVORK5CYII=); - width:36px; - height:32px; - } - - .diagram-subscription.subscribed button.eye { - filter:invert(100%) opacity(50%); - } - `; - iframe.contentDocument.head.appendChild(style); - - // add the corresponding javascript - const script = document.createElement('script'); - script.textContent = ` - const btnSubscriptions = document.querySelector('div.diagram-subscription'); - const btnSubscribe = document.querySelector('.diagram-subscription button.subscribe'); - const btnUnsubscribe = document.querySelector('.diagram-subscription button.unsubscribe'); - - function getSubscriptionState() { - var csrf = phorge_extension.csrf; - var data = new URLSearchParams(); - var phid = phorge_extension.diagramPHID; - data.append('__csrf__', csrf); - data.append('__form__', '1'); - data.append('__ajax__', 'true'); - - // send second AJAX call to verify if subscription was executed - var responseSubscription = new XMLHttpRequest(); - responseSubscription.onreadystatechange = function () { - if (this.readyState == 4 && this.status == 200) { - var result = JSON.parse(responseSubscription.responseText); - - btnSubscriptions.classList.remove('subscribed'); - btnSubscriptions.classList.remove('unsubscribed'); - - if (result.subscribed == true) { - btnSubscriptions.classList.add('subscribed'); - } else { - btnSubscriptions.classList.add('unsubscribed'); - } - } - } - - var url = phorge_extension.baseURI - + '/subscribed/request/' - + phorge_extension.diagramPHID - + '/'; - - responseSubscription.overrideMimeType("application/json"); - responseSubscription.open('POST', url, true); - responseSubscription.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); - responseSubscription.send(data); - } - - function subscription(addDelete) { - var csrf = phorge_extension.csrf; - var data = new URLSearchParams(); - var phid = phorge_extension.diagramPHID; - data.append('__csrf__', csrf); - data.append('__form__', '1'); - data.append('__ajax__', 'true'); - - var requestSubscription = new XMLHttpRequest(); - requestSubscription.onreadystatechange = function () { - if (this.readyState == 4 && this.status == 200) { - getSubscriptionState(); - } - } - requestSubscription.overrideMimeType("application/json"); - requestSubscription.open('POST', "/subscriptions/" + addDelete + "/" + phid + "/", true); - requestSubscription.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); - requestSubscription.send(data); - } - - btnSubscribe.addEventListener('click', (event) => { - event.stopPropagation(); - subscription('add'); - }); - - btnUnsubscribe.addEventListener('click', (event) => { - event.stopPropagation(); - subscription('delete'); - }); - - if (phorge_extension.diagramPHID !== "") { - getSubscriptionState(); - } - `; - iframe.contentDocument.body.appendChild(script); - return div; } \ No newline at end of file diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php index e9bd8a1..e0af898 100644 --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -1,80 +1,89 @@ array( // url to image data "data/(?PPHID-DGVN-[a-z0-9]{20})" . "(?:/(?P[^/]+))?" . "(?:/|\?|$).*" => "DiagramController", // version info requests "version/DIAG(?P(\d+))" . "/(?P(\d+))" . "(?:/|\?|$).*" => "DiagramController", // draw.io iframe urls "(?:(?PDIAG(\d+))/(?P\d+)/?)" . "(?Piframe)" . "(?:/|\?|$).*" => "DiagramController", // draw.io iframe urls "(?:(?PDIAG(\d+))/?)?" . "(?Piframe)" . "(?:/|\?|$).*" => "DiagramController", // url with diagram id and version in it "(?PDIAG(\d+))" . "/(?P\d+)" . "(?:/|\?|$).*" => "DiagramController", // url with diagram id in it "(?PDIAG(\d+))" . "(?:/|\?|$).*" => "DiagramController", // url for validating subscription "subscribed/request/" - . "(?P[^/]+)/" => 'DiagramController', + . "(?P[^/]+)/" + => 'DiagramController', + + // url for loading/initializing diagram content + "(?PloadJsExtension)/" + . "(/?(?P[^/]*))?" + . "(/?(?P[^/]*))?" + . "(/?(?P[^/]*))?" + . ".*" + => 'DiagramController', // other urls ".*" => "DiagramController", ) ); } } \ No newline at end of file diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index 86234da..4508f02 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,571 +1,947 @@ setRequest($request); // detetermine if GET or POST HTTP call if ($request->isHTTPPost()) { // process POST calls (like save action) return $this->handleHttpPostCall($request); } // determine type of URL by means of DiagramApplication route parameters $diagramphid = $request->getURIData('diagramphid'); $diagramid = $request->getURIData('diagramid'); $versioneddiagramid = $request->getURIData('versioneddiagramid'); $version = $request->getURIData('version'); $route = $request->getURIData('route'); $versioninfoDiagramID = $request->getURIData('versioninfodiagram'); $versioninfoPage = $request->getURIData('versioninfopage'); + $loadDiagramName = $request->getURIData('loadDiagramName'); + $loadDiagramPHID = $request->getURIData('loadDiagramPHID'); + $loadDiagramVersion = $request->getURIData('loadDiagramVersion'); if (isset($diagramphid) && !empty(trim($diagramphid))) { // return PNG image data $diagram = id(new DiagramVersion())->loadByDiagramPHID($diagramphid); if ($diagram !== null) { $response = new AphrontFileResponse(); $response->setMimeType('image/png'); $response->setContent($diagram->getData()); return $response; } } if (isset($versioninfoDiagramID) && !empty(trim($versioninfoDiagramID))) { // return diagram version info if (!isset($versioninfoPage) || empty(trim($versioninfoPage))) { // versioninfoPage was dismissed -> initialize to 1 $versioninfoPage = "1"; } - $diagramVersions = id(new DiagramVersion())->loadByDiagramID($versioninfoDiagramID); + $diagramVersions = id(new DiagramVersion())->loadByDiagramID( + $versioninfoDiagramID); + if ($diagramVersions !== null) { $result = []; $viewer = $request->getViewer(); // determine total count of versions $totalcount = count($diagramVersions); // filter out some of the versions we want to show $pageSize = 10; $diagramVersions = array_slice($diagramVersions, ($versioninfoPage - 1) * $pageSize, $pageSize ); // calculate number of pages $totalpages = ceil($totalcount / $pageSize); // create menu-items foreach ($diagramVersions as $diagramVersion) { $author = $diagramVersion->getAuthorPHID(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array( $author )) ->executeOne(); $dateModified = $diagramVersion->getDateModified(); $result[] = array( "id" => $diagramVersion->getVersion(), "datetime" => phabricator_datetime($dateModified, $viewer), "author" => $user->getUsername() ); } // reply back $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "data" => $result, "pagecount" => $totalpages, "nopager" => $totalcount <= $pageSize )); return $response; } else { // version info requested for inexistant diagram $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "data" => array(), "pagecount" => 0 )); return $response; } } + if ($route == 'loadJsExtension') { + $response = new AphrontFileResponse(); + $response->setMimeType('application/javascript'); + + $base64_data = ""; + if (isset($loadDiagramName) && !empty(trim($loadDiagramName))) { + $diagram_id = (int) substr($loadDiagramName, strlen("DIAG")); + if (isset($loadDiagramVersion) && !empty(trim($loadDiagramVersion))) { + $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( + $diagram_id, + $loadDiagramVersion); + } else { + $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( + $diagram_id); + } + if ($diagramVersion) { + $data = $diagramVersion->getData(); + $base64_data = base64_encode($data); + } + } + + $response->setContent('loadJsExtension("' + . $loadDiagramName + . '", "' + . $loadDiagramPHID + . '", "' + . $loadDiagramVersion + . '", "' + . $base64_data + . '");'); + return $response; + } + $root = ''; $file = rtrim($request->getPath(), '/'); $root = dirname(phutil_get_library_root('diagram')); // determine from which application area the file should be loaded: // 1) Phorge extension source // or 2) drawio source if ($route == 'iframe') { // load from drawio source if ($file == '/diagram/iframe') $file .= '/index.html'; if ($versioneddiagramid != null && $version != null) { - $file = preg_replace("/^\/diagram\/" . $versioneddiagramid . "\/" . $version ."iframe\//", + $file = preg_replace( + "/^\/diagram\/$versioneddiagramid\/$version" ."iframe\//", "data/drawio/src/main/webapp/", $file); } else { - $file = preg_replace("/^\/diagram\/(" . $diagramid . "\/?)?iframe\//", + $file = preg_replace("/^\/diagram\/($diagramid\/?)?iframe\//", "data/drawio/src/main/webapp/", $file); } } else { // load from extension source if (rtrim($file, '/') == '/diagram') { return $this->showApplication($request); } if ($versioneddiagramid !== null && $version !== null) { - $file = preg_replace('/^\/diagram\/' . $versioneddiagramid . '\/'. $version . '\/?/', + $file = preg_replace( + '/^\/diagram\/' . $versioneddiagramid . '\/'. $version . '\/?/', 'data/', $file ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { - $file = preg_replace('/^\/diagram\/(' . $diagramid . '\/)?/', + $file = preg_replace( + '/^\/diagram\/(' . $diagramid . '\/)?/', 'data/', $file ); } } + // check if we are trying to load "iframe loader" files, + // if so, correct the path accordingly + if ($file == "data/drawio/src/main/webapp/index.html") { + return $this->showIframe($request); + } + else + if ($file == "data/drawio/src/main/webapp/iframe.css" + || $file == "data/drawio/src/main/webapp/iframe-toolbtn.css" + || $file == "data/drawio/src/main/webapp/iframe1.js" + || $file == "data/drawio/src/main/webapp/iframe2.js" + || $file == "data/drawio/src/main/webapp/iframe-toolbtn.js") { + $file = str_replace("drawio/src/main/webapp/", "", $file); + } + // determine full path $path = $root . '/' . $file; if (file_exists($path) == false || is_readable($path) == false) { if (preg_match('/^data\/DIAG(\d+)$/', $file, $matches)) { $diagram_id = (int) $matches[1]; if ($version === null) { $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( $diagram_id); } else { $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( $diagram_id, $version); } if ($diagramVersion) { $data = $diagramVersion->getData(); $base64_data = base64_encode($data); $diagram = id(new Diagram())->loadByID($diagram_id); return $this->showApplication( $request, 'DIAG' . $diagram_id, $diagram->getPHID(), $version ?? "", $base64_data ); } } // Invalid URL $response = id(new Aphront404Response()); return $response; } else { // process Iframe content switch (pathinfo($file, PATHINFO_EXTENSION)) { case 'html': $response = id(new PlainHtmlWebpageResponse()) ->setFrameable(true) ->setContent(file_get_contents($path)); break; case 'js': $response = new AphrontFileResponse(); $response->setMimeType('application/javascript'); break; case 'css': $response = new AphrontFileResponse(); $response->setMimeType('text/css'); break; case 'txt': $response = new AphrontFileResponse(); $response->setMimeType('text/plain'); break; case 'png': $response = new AphrontFileResponse(); $response->setMimeType('image/png'); break; case 'gif': $response = new AphrontFileResponse(); $response->setMimeType('image/gif'); break; case 'jpg': case 'jpeg': $response = new AphrontFileResponse(); $response->setMimeType('image/jpeg'); break; default: $response = new AphrontFileResponse(); $response->setMimeType('application/octet-stream'); break; } try { $response->setContent(file_get_contents($path)); } catch (Exception $e) { $response->setContent($route); } return $response; } } /** * Compares the draw.io tEXt metadata from 2 PNG base64 strings. * The content looks like this: * * * * * ... * * * * * * * The modified and etag attributes of mxfile will always be different. * They are cut out before the 2 strings are compared. */ public static function equalPngMetaData($base64_1, $base64_2) { $base64 = array($base64_1, $base64_2); $textData = array(); for ($i = 0; $i < 2; $i++) { $data = base64_decode($base64[$i]); $fp = fopen('data://text/plain;base64,' . base64_encode($data), 'rb'); $sig = fread($fp, 8); if ($sig != "\x89PNG\x0d\x0a\x1a\x0a") { fclose($fp); return false; } $textData[$i] = array(); while (!feof($fp)) { try { $chunk = unpack('Nlength/a4type', fread($fp, 8)); } catch (Exception $e) { // invalid base64 data return false; } if ($chunk['type'] == 'IEND') break; if ($chunk['type'] == 'tEXt') { list($key, $val) = explode("\0", fread($fp, $chunk['length'])); if ($key == 'mxfile') { // Decode the URL-encoded XML data $decodedVal = urldecode($val); // Load the XML and remove the modified and etag attributes $xml = simplexml_load_string($decodedVal); unset($xml->attributes()->modified); unset($xml->attributes()->etag); // Save the modified XML as the value $val = $xml->asXML(); } $textData[$i][$key] = $val; fseek($fp, 4, SEEK_CUR); } else { fseek($fp, $chunk['length'] + 4, SEEK_CUR); } } fclose($fp); } if (isset($textData[0]['mxfile']) && isset($textData[1]['mxfile'])) { // Both arrays contain the mxfile key, compare their values return $textData[0]['mxfile'] == $textData[1]['mxfile']; } else { - // At least one of the arrays does not contain the mxfile key, return false + // At least one of the arrays doesn't contain mxfile key, return false return false; } } /** * Processes HTTP POST calls from Diagram application, like 'Save' action */ private function handleHttpPostCall(AphrontRequest $request) { $subscriptionphid = $request->getURIData('subscriptionphid'); if (isset($subscriptionphid) && !empty(trim($subscriptionphid))) { // get list of subscriber for specified diagram phid $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $subscriptionphid); // verify if viewer is subscriber $viewer = $request->getViewer(); if ($viewer == null) { $isSubscribed = false; } else { $isSubscribed = in_array($viewer->getPHID(),$subscribers); } // reply back $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "subscribed" => $isSubscribed )); return $response; } $base64_data = $request->getStr("data"); $diagram_id = $request->getStr("diagramID"); // cut off "data:image/png;base64," $base64_data = substr($base64_data, strpos($base64_data, ',') + 1); if ($diagram_id != "") { // check if we are trying to save the same data as the current data $diagram = id(new DiagramVersion())->loadLatestByDiagramID($diagram_id); if ($diagram !== null) { $data = $diagram->getData(); $old_data = base64_encode($data); if (DiagramController::equalPngMetaData($base64_data, $old_data)) { // data hasn't been modified // => do not create new version $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "OK", "DiagramID" => $diagram->getDiagramID(), "Version" => $diagram->getVersion() )); return $response; } } } // Set the options for the new file $options = array( 'name' => 'diagram.png', 'viewPolicy' => PhabricatorPolicies::POLICY_USER, 'mime-type' => 'image/png', 'actor' => $this->getViewer(), 'diagramID' => $diagram_id ); try { // Create the new file object $diagram = DiagramVersion::newFromFileData($base64_data, $options); $diagram->publishNewVersion($request, $diagram->getDiagramID()); $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "OK", "DiagramID" => $diagram->getDiagramID(), "Version" => $diagram->getVersion() )); return $response; } catch (Exception $e) { $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "ERROR", "Error" => $e->getMessage(), )); return $response; } } /** * Verifies if the given base64 data is draw.io compatible */ public static function isDrawioPngBase64($base64) { $data = base64_decode($base64); $fp = fopen('data://text/plain;base64,' . base64_encode($data), 'rb'); $sig = fread($fp, 8); if ($sig != "\x89PNG\x0d\x0a\x1a\x0a") { fclose($fp); return false; } while (!feof($fp)) { try { $chunk = unpack('Nlength/a4type', fread($fp, 8)); } catch (Exception $e) { // invalid base64 data return false; } if ($chunk['type'] == 'IEND') break; if ($chunk['type'] == 'tEXt') { list($key, $val) = explode("\0", fread($fp, $chunk['length'])); if ($key == 'mxfile') { fclose($fp); return true; } fseek($fp, 4, SEEK_CUR); } else { fseek($fp, $chunk['length'] + 4, SEEK_CUR); } } fclose($fp); return false; } /** * Shows the draw.io application integrated in Phorge's layout */ private function showApplication( AphrontRequest $request, string $diagramName = null, string $diagramPHID = null, string $diagramVersion = null, string $diagramBase64 = null ) { $applicationUrl = "/" . explode("/", $request->getPath())[1]; $content = phutil_tag( 'div', array(), array( phutil_tag( 'div', array( 'id' => 'mainScreen', )), phutil_tag('div', array(), array( phutil_tag( 'img', array( 'class' => 'drawio', )), - phutil_tag('div', - array( - 'id' => 'loadingtext', - 'class' => 'geBlock', - 'style' => 'margin-top:80px;' - . 'text-align:center;' - . 'min-width:50%;' - . 'height:100vh;', - ), - array( - phutil_tag('h1', - array(), - 'Flowchart Maker and Online Diagram Software' - ), - phutil_tag('p', - array( - 'style' => 'width: 800px;' - . 'position: sticky;' - . 'left: calc(50% - 400px);', - ), - 'draw.io is free online diagram software. ' - . 'You can use it as a flowchart maker, network diagram ' - . 'software, to create UML online, as an ER diagram tool, ' - . 'to design database schema, to build BPMN online, as a ' - . 'circuit diagram maker, and more. draw.io can import ' - . '.vsdx, Gliffy' ."\u{2122}" . ' and Lucidchart' - . "\u{2122}" .' files . ' - ), - phutil_tag( - 'h2', - array( - 'id' => 'geStatus', - ), - 'Loading...' - ), - phutil_tag( - 'div', - array( - 'id' => 'spinnerLoading', - )) - )) )), phutil_tag( 'script', array( - 'src' => 'phorge_extension.js' + 'src' => 'phorge_extension.js', + 'defer' => true ), '' ), phutil_tag( 'script', - array(), - phutil_safe_html('loadJsExtension("' - . $diagramName - . '", "' - . $diagramPHID - . '", "' - . $diagramVersion - . '", "' - . $diagramBase64 - . '");') + array( + 'src' => $applicationUrl . '/loadJsExtension/' + . $diagramName + . '/' + . $diagramPHID + . '/' + . $diagramVersion + . '/', + 'defer' => true + ), + '' ), phutil_tag( 'div', array( 'class' => 'crumbs', 'style' => 'top:48px;' . 'margin-left: 4px;' . 'position: fixed;' . 'font-weight: bold;' ), array( phutil_tag( 'a', array( 'href' => $applicationUrl ), array( phutil_tag( 'span', array( 'class' => 'phui-font-fa fa-sitemap', 'style' => 'padding-right:5px;' )) )), phutil_tag( 'a', array( 'href' => $applicationUrl ), 'Diagram' ), phutil_tag( 'span', array( 'class' => 'diagramName', 'style' => 'display:none' ), array( phutil_tag( 'span', array( 'style' => 'margin: 5px;' . 'opacity: .5;' ), '>' ), phutil_tag( 'a', array(), '' ), phutil_tag( 'span', array( 'class' => 'version', 'style' => 'margin-left: 8px;' . 'color: #999;'), ''), )) )) )); $view = id(new PhabricatorStandardPageView()) ->setRequest($request) ->setController($this) ->setDeviceReady(true) ->setTitle("Diagrams") ->appendChild($content); $response = id(new AphrontWebpageResponse()) ->setContent($view->render()); return $response; } + + /** + * Shows the internal draw.io application + */ + private function showIframe( + AphrontRequest $request + ) { + $content = phutil_tag( + 'html', + array(), + array( + phutil_tag( + 'head', + array(), + array( + phutil_tag( + 'title', + array(), + 'Flowchart Maker & Online Diagram Software' + ), + phutil_tag( + 'meta', + array( + 'charset' => 'utf-8' + ) + ), + phutil_tag( + 'meta', + array( + 'http-equiv' => 'content-type', + 'content' => 'text/html; charset=utf-8' + ) + ), + phutil_tag( + 'meta', + array( + 'name' => 'viewport', + 'content' => 'width=device-width, initial-scale=1.0, ' + . 'maximum-scale=1.0, user-scalable=no' + ) + ), + phutil_tag( + 'meta', + array( + 'name' => 'mobile-web-app-capable', + 'content' => 'yes' + ) + ), + phutil_tag( + 'link', + array( + 'rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => 'iframe.css' + ) + ), + phutil_tag( + 'link', + array( + 'rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => 'iframe-toolbtn.css' + ) + ), + phutil_tag( + 'link', + array( + 'rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => 'styles/grapheditor.css' + ) + ), + phutil_tag( + 'script', + array( + 'type' => 'text/javascript', + 'src' => 'iframe1.js', + 'defer' => true + ) + ) + ) + ), + phutil_tag( + 'body', + array( + 'class' => 'geEditor' + ), + array( + phutil_tag( + 'div', + array( + 'id' => 'geinfo' + ), + phutil_tag( + 'div', + array( + 'class' => 'geBlock' + ), + array( + phutil_tag( + 'h1', + array(), + 'Flowchart Maker and Online Diagram Software' + ), + phutil_tag( + 'p', + array(), + 'draw.io is free online diagram software. You can use ' + . 'it as a flowchart maker, network diagram software, ' + . 'to create UML online, as an ER diagram tool, to ' + . 'design database schema, to build BPMN online, as a ' + . 'circuit diagram maker, and more. draw.io can import ' + . '.vsdx, Gliffy™ and Lucidchart™ files.' + ), + phutil_tag( + 'h2', + array( + 'id' => 'gestatus' + ), + 'Loading...' + ), + phutil_tag( + 'div', + array( + 'class' => 'init-spinner' + ), + phutil_tag( + 'div', + array( + 'class' => 'content' + ), + array( + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(0deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -1s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(27deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.923077ss;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(55deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.846154s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(83deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.769231s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(110deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.692308s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(138deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.615385s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(166deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.538462s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(193deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.461538s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(221deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.384615s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(249deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.307692s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(276deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.230769s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(304deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.153846s;' + ) + ) + ), + phutil_tag( + 'div', + array( + 'class' => 'spike', + 'style' => 'transform: rotate(332deg)' + . ' translatex(13px);' + ), + phutil_tag( + 'div', + array( + 'class' => 'animator', + 'style' => 'animation-delay: -0.0769231s;' + ) + ) + ) + ) + ) + ) + ) + ), + ), + phutil_tag( + 'script', + array( + 'type' => 'text/javascript', + 'src' => 'iframe2.js', + 'defer' => true + ) + ), + phutil_tag( + 'script', + array( + 'type' => 'text/javascript', + 'src' => 'iframe-toolbtn.js', + 'defer' => true + ) + ) + ) + ) + ) + ); + + $response = id(new PlainHtmlWebpageResponse()) + ->setFrameable(true) + ->setContent($content); + + return $response; + } } diff --git a/src/response/PlainHtmlWebpageResponse.php b/src/response/PlainHtmlWebpageResponse.php index 4272260..d114446 100644 --- a/src/response/PlainHtmlWebpageResponse.php +++ b/src/response/PlainHtmlWebpageResponse.php @@ -1,58 +1,63 @@ content = $content; return $this; } public function setUnexpectedOutput($unexpected_output) { $this->unexpectedOutput = $unexpected_output; return $this; } public function getUnexpectedOutput() { return $this->unexpectedOutput; } public function buildResponseString() { $unexpected_output = $this->getUnexpectedOutput(); if (phutil_nonempty_string($unexpected_output)) { // in case we get some unexpected output (e.g. a stacktraced error) // the output is shown on top of the screen in a red banner $style = array( 'background: linear-gradient(180deg, #eeddff, #ddbbff);', 'white-space: pre-wrap;', 'z-index: 200000;', 'position: relative;', 'padding: 16px;', 'font-family: monospace;', 'text-shadow: 1px 1px 1px white;' ); $unexpected_header = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $unexpected_output); } else { $unexpected_header = ''; } + $content = $this->content; + if (strcasecmp(substr($content, 0, 5), "content; + return hsprintf('%s', $unexpected_header) . $content; } }