diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..1de328a --- /dev/null +++ b/.arcconfig @@ -0,0 +1,6 @@ +{ + "phabricator.uri": "https://we.phorge.it/", + "load": [ + "src/" + ] +} diff --git a/.gitignore b/.gitignore index 5126429..9d4ac37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ -/data/drawio - -/src/__phutil_library_init__.php -/src/__phutil_library_map__.php +/drawio /src/.phutil_module_cache diff --git a/README.md b/README.md index 7f239e6..c300fa4 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,38 @@ 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. +1) `git clone` this repository somewhere safe: `` +2) CD to `` and `git clone https://github.com/jgraph/drawio.git && cd drawio && git switch v24.2.5` +3) `/bin/storage upgrade` +4) In Phorge's `conf/local/local.json` add the path to the `src/` dir to the entry `load-libraries`, + something like this: +``` + ... + "load-libraries": [ + "/somewhere/safe/diagrams/src/" + ], + ... +``` +5) Diagrams application is available under "More Applications" in Phorge. You may add it to your navigator menu via "Edit Menu" 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.css b/data/iframe.css deleted file mode 100644 index 3a7bdfe..0000000 --- a/data/iframe.css +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 5a3ed22..0000000 --- a/data/iframe1.js +++ /dev/null @@ -1,144 +0,0 @@ -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 deleted file mode 100644 index ff355fd..0000000 --- a/data/iframe2.js +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 6044413..0000000 --- a/data/phorge_extension.js +++ /dev/null @@ -1,246 +0,0 @@ -var phorge_extension = {}; - -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); - - setupButtonsInMenuToolbar(); - } - else if (msg.event == 'export') { - saveFlowchart(name, msg.data, iframe); - } - 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=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('toolbtn-version-dropdown'); - - // create the dropdown toggle button - const toggle = document.createElement('button'); - 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('toolbtn-version-dropdown-menu'); - menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); - - // create the version list - const versionList = document.createElement('ul'); - versionList.classList.add('toolbtn-version-list'); - - menu.appendChild(versionList); - - // create the prev and next buttons - const prevBtn = document.createElement('button'); - prevBtn.classList.add('toolbtn-version-prev-btn'); - prevBtn.textContent = '<'; - - const nextBtn = document.createElement('button'); - 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); - - return dropdown; -} - -function setupSubscriptionButtonInMenuToolbar(iframe, btnLeft) { - // generate 'Subscribe/Unsubscribe' button - // create grouping div element - const div = document.createElement('div'); - 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); - - return div; -} \ No newline at end of file diff --git a/rsrc/behavior-diagram-extension.js b/rsrc/behavior-diagram-extension.js new file mode 100644 index 0000000..eb99c41 --- /dev/null +++ b/rsrc/behavior-diagram-extension.js @@ -0,0 +1,271 @@ +/** + * @provides javelin-behavior-diagram-extension + */ + +JX.behavior('diagram-extension', function(config) { + + var phorge_extension = {}; + + function createIframe() { + 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'; + + var receive = function (evt) { + if (evt.data.length > 0) { + var msg = JSON.parse(evt.data); + if (msg.event == 'init') { + if (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: 'data:image/png;base64,' + phorge_extension.diagramBase64 + }), '*'); + } + else if (msg.event == 'load') { + // enable Mathematical Typesettings by default + iframe.contentWindow.sb.editorUi.setMathEnabled(true); + + setupButtonsInMenuToolbar(); + } + else if (msg.event == 'export') { + saveFlowchart(name, msg.data, iframe); + } + 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.contentWindow.addEventListener('DOMContentLoaded', addToolbarResourcesToIframe); + 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 addToolbarResourcesToIframe() { + var head = this.document.getElementsByTagName('head')[0]; + + var script = this.document.createElement('script'); + script.src = config.toolbarJs; + head.appendChild(script); + + var styles = this.document.createElement('link'); + styles.setAttribute('type', 'text/css'); + styles.setAttribute('rel', 'stylesheet'); + styles.setAttribute('href', config.toolbarCss); + head.appendChild(styles); + } + + function init(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=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; + + createIframe(); + } + + 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('toolbtn-version-dropdown'); + + // create the dropdown toggle button + const toggle = document.createElement('button'); + 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('toolbtn-version-dropdown-menu'); + menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); + + // create the version list + const versionList = document.createElement('ul'); + versionList.classList.add('toolbtn-version-list'); + + menu.appendChild(versionList); + + // create the prev and next buttons + const prevBtn = document.createElement('button'); + prevBtn.classList.add('toolbtn-version-prev-btn'); + prevBtn.textContent = '<'; + + const nextBtn = document.createElement('button'); + 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); + + return dropdown; + } + + function setupSubscriptionButtonInMenuToolbar(iframe, btnLeft) { + // generate 'Subscribe/Unsubscribe' button + // create grouping div element + const div = document.createElement('div'); + 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); + + return div; + } + + if (config.initParams) { + init.apply(undefined, config.initParams); + } + + window.__diagram = phorge_extension; + +}); diff --git a/data/iframe-toolbtn.css b/rsrc/iframe-toolbtn.css similarity index 97% rename from data/iframe-toolbtn.css rename to rsrc/iframe-toolbtn.css index 06ba82f..4fd847e 100644 --- a/data/iframe-toolbtn.css +++ b/rsrc/iframe-toolbtn.css @@ -1,113 +1,117 @@ -/* begin stylesheets for version toolbutton */ +/** + * @provides diagram-css-iframe-toolbtn + */ + +/* 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/rsrc/iframe-toolbtn.js similarity index 97% rename from data/iframe-toolbtn.js rename to rsrc/iframe-toolbtn.js index b56c27b..26eb766 100644 --- a/data/iframe-toolbtn.js +++ b/rsrc/iframe-toolbtn.js @@ -1,251 +1,257 @@ +/** + * @provides diagram-js-iframe-toolbtn + */ + /** * global dicttionary containing all configuration of all installed toolbuttons */ const toolbtn = {}; +const phorge_extension = parent.__diagram; + /** * 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; + 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 +new ToolButtonSubscription('subscription'); diff --git a/rsrc/remarkup-image.css b/rsrc/remarkup-image.css new file mode 100644 index 0000000..2ce13b2 --- /dev/null +++ b/rsrc/remarkup-image.css @@ -0,0 +1,8 @@ +/** + * @provides diagram-remarkup-image-css + */ + +.diagram-container > .diagram-content { + max-width: 100%; + cursor: pointer; +} diff --git a/rsrc/remarkup-image.js b/rsrc/remarkup-image.js new file mode 100644 index 0000000..dd4c825 --- /dev/null +++ b/rsrc/remarkup-image.js @@ -0,0 +1,27 @@ +/** + * @provides diagram-remarkup-image-js + */ + +JX.onload(function() { + + var singleClickTimeout = null; + + JX.Stratcom.listen( + 'click', + ['diagram-remarkup-image'], + function(evt) { + var detail = evt.getRawEvent().detail; + + if (detail === 1) { + singleClickTimeout = window.setTimeout(function() { + window.open('/diagram/data/' + evt.getTarget().dataset.diagramVersion); + singleClickTimeout = null; + }, 300); + } else if (detail === 2) { + window.clearTimeout(singleClickTimeout); + window.open('/diagram/DIAG' + evt.getTarget().dataset.diagramId); + } + } + ); + +}); diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php new file mode 100644 index 0000000..f218901 --- /dev/null +++ b/src/__phutil_library_init__.php @@ -0,0 +1,3 @@ + 2, + 'class' => array( + 'Diagram' => 'storage/Diagram.php', + 'DiagramApplication' => 'application/DiagramApplication.php', + 'DiagramCelerityResources' => 'celerity/DiagramCelerityResources.php', + 'DiagramContentTransaction' => 'xaction/DiagramContentTransaction.php', + 'DiagramController' => 'controller/DiagramController.php', + 'DiagramDAO' => 'storage/DiagramDAO.php', + 'DiagramPHIDType' => 'phid/DiagramPHIDType.php', + 'DiagramPatchList' => 'storage/patch/DiagramPatchList.php', + 'DiagramReplyHandler' => 'mail/DiagramReplyHandler.php', + 'DiagramSchemaSpec' => 'storage/DiagramSchemaSpec.php', + 'DiagramSearchConduitAPIMethod' => 'conduit/DiagramSearchConduitAPIMethod.php', + 'DiagramTransaction' => 'storage/DiagramTransaction.php', + 'DiagramTransactionEditor' => 'editor/DiagramTransactionEditor.php', + 'DiagramTransactionType' => 'xaction/DiagramTransactionType.php', + 'DiagramUploadConduitAPIMethod' => 'conduit/DiagramUploadConduitAPIMethod.php', + 'DiagramVersion' => 'storage/DiagramVersion.php', + 'PhabricatorDiagramQuery' => 'query/PhabricatorDiagramQuery.php', + 'PhabricatorDiagramTransactionQuery' => 'query/PhabricatorDiagramTransactionQuery.php', + 'PhabricatorDiagramVersionQuery' => 'query/PhabricatorDiagramVersionQuery.php', + 'PhabricatorRemarkupDiagramRule' => 'remarkup/PhabricatorRemarkupDiagramRule.php', + 'PlainHtmlWebpageResponse' => 'response/PlainHtmlWebpageResponse.php', + ), + 'function' => array(), + 'xmap' => array( + 'Diagram' => array( + 'DiagramDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorSubscribableInterface', + ), + 'DiagramApplication' => 'PhabricatorApplication', + 'DiagramCelerityResources' => 'CelerityResourcesOnDisk', + 'DiagramContentTransaction' => 'DiagramTransactionType', + 'DiagramController' => 'PhabricatorController', + 'DiagramDAO' => 'PhabricatorLiskDAO', + 'DiagramPHIDType' => 'PhabricatorPHIDType', + 'DiagramPatchList' => 'PhabricatorSQLPatchList', + 'DiagramReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'DiagramSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'DiagramSearchConduitAPIMethod' => 'ConduitAPIMethod', + 'DiagramTransaction' => 'PhabricatorModularTransaction', + 'DiagramTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'DiagramTransactionType' => 'PhabricatorModularTransactionType', + 'DiagramUploadConduitAPIMethod' => 'ConduitAPIMethod', + 'DiagramVersion' => array( + 'DiagramDAO', + 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorDiagramQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDiagramTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorDiagramVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRemarkupDiagramRule' => 'PhabricatorObjectRemarkupRule', + 'PlainHtmlWebpageResponse' => 'AphrontHTMLResponse', + ), +)); diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php index e0af898..28ca3fb 100644 --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -1,89 +1,81 @@ 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', - // url for loading/initializing diagram content - "(?PloadJsExtension)/" - . "(/?(?P[^/]*))?" - . "(/?(?P[^/]*))?" - . "(/?(?P[^/]*))?" - . ".*" - => 'DiagramController', - // other urls ".*" => "DiagramController", ) ); } -} \ No newline at end of file +} diff --git a/src/celerity/DiagramCelerityResources.php b/src/celerity/DiagramCelerityResources.php new file mode 100644 index 0000000..c2fd62c --- /dev/null +++ b/src/celerity/DiagramCelerityResources.php @@ -0,0 +1,15 @@ + array( + 'behavior-diagram-extension.js' => 'adc38334', + 'iframe-toolbtn.css' => '35ad6f49', + 'iframe-toolbtn.js' => '26d75a35', + 'remarkup-image.css' => '42b46bf1', + 'remarkup-image.js' => '64e7e9e1', + ), + 'symbols' => array( + 'diagram-css-iframe-toolbtn' => '35ad6f49', + 'diagram-js-iframe-toolbtn' => '26d75a35', + 'diagram-remarkup-image-css' => '42b46bf1', + 'diagram-remarkup-image-js' => '64e7e9e1', + 'javelin-behavior-diagram-extension' => 'adc38334', + ), + 'requires' => array(), + 'packages' => array(), +); diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index 65220bf..cacf8d0 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,947 +1,524 @@ 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); 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\//", - "data/drawio/src/main/webapp/", + "drawio/src/main/webapp/", $file); } else { $file = preg_replace("/^\/diagram\/($diagramid\/?)?iframe\//", - "data/drawio/src/main/webapp/", + "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 . '\/?/', 'data/', $file ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { $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()) + ->setDisableContentSecurityPolicy(true) ->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)); + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); + $response->setLastModified(time()); + $response->setCanCDN(true); } 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 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 + string $diagramName = '', + string $diagramPHID = '', + string $diagramVersion = '', + string $base64_data = '' ) { $applicationUrl = "/" . explode("/", $request->getPath())[1]; + $behaviorConfig = array(); + $behaviorConfig['initParams'] = array( + $diagramName, + $diagramPHID, + $diagramVersion, + $base64_data + ); + $behaviorConfig['toolbarCss'] = celerity_get_resource_uri('/iframe-toolbtn.css', 'diagram-resources'); + $behaviorConfig['toolbarJs'] = celerity_get_resource_uri('/iframe-toolbtn.js', 'diagram-resources'); + + require_celerity_resource('javelin-behavior'); + Javelin::initBehavior('diagram-extension', $behaviorConfig, 'diagram-resources'); + $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( - 'script', - array( - 'src' => 'phorge_extension.js', - 'defer' => true - ), - '' - ), - phutil_tag( - 'script', - 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/remarkup/PhabricatorRemarkupDiagramRule.php b/src/remarkup/PhabricatorRemarkupDiagramRule.php index efbf063..c9c362f 100644 --- a/src/remarkup/PhabricatorRemarkupDiagramRule.php +++ b/src/remarkup/PhabricatorRemarkupDiagramRule.php @@ -1,102 +1,103 @@ getEngine()->getConfig('viewer'); $objects = id(new PhabricatorDiagramVersionQuery()) ->setViewer($viewer) ->withDiagramIDs($ids) ->execute(); return $objects; } protected function renderObjectEmbed( $diagram, PhabricatorObjectHandle $handle, $options) { if ($options) { $params = explode(',', $options); $params = array_map('trim', $params); } else { $params = array(); } - // Get the file PHID for the Diagram object. - $file_phid = $diagram->getPHID(); - // Generate the appropriate HTML using the data from the Diagram and // file objects. $style = ''; $class = 'diagram-content'; $alt = ''; $has_layout = false; foreach ($params as $param) { if (strpos($param, '=') !== false) { list($key, $value) = explode('=', $param, 2); } else { $key = $param; $value = null; } switch ($key) { case 'layout': $has_layout = true; if ($value === 'left') { $class .= ' phabricator-remarkup-embed-layout-left'; } else if ($value === 'right') { $class .= ' phabricator-remarkup-embed-layout-right'; } break; case 'float': $class .= ' phabricator-remarkup-embed-float-left'; break; case 'size': if ($value === 'full') { $style .= 'width: 100%;'; } break; case 'alt': $alt = phutil_escape_html($value); break; } } if ($has_layout == false) { $class .= ' phabricator-remarkup-embed-layout-left'; } $output = phutil_tag( 'div', array( 'class' => 'diagram-container', ), phutil_tag( 'img', array( 'style' => $style, 'class' => $class, - 'src' => $diagram->getViewURI(), + 'src' => 'data:image/png;base64,' . $diagram->getBase64Data(), 'alt' => $alt, - 'ondblclick' => 'window.open("/diagram/DIAG' - . $diagram->getDiagramID() - . '", "_blank")', + 'data-sigil' => 'diagram-remarkup-image', + 'data-diagram-version' => $diagram->getPHID(), + 'data-diagram-id' => $diagram->getDiagramID(), + 'title' => 'Double click to edit...' ) ) ); return $output; } }