diff --git a/.gitignore b/.gitignore index ff6a1fa..5126429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /data/drawio /src/__phutil_library_init__.php /src/__phutil_library_map__.php +/src/.phutil_module_cache diff --git a/data/phorge_extension.js b/data/phorge_extension.js index d2e417b..003a2f7 100644 --- a/data/phorge_extension.js +++ b/data/phorge_extension.js @@ -1,429 +1,579 @@ 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'); 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"; + 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 = '(#' + 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.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, diagramVersion, diagramBase64) { + document.querySelector('#mainScreen').appendChild(iframe); + 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.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 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.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") { + 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; + 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 - ) + - 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); +} + +function setupVersionDropDownInMenuToolbar(iframe, btnLeft) { // generate 'Select Version' button // create the dropdown element const dropdown = document.createElement('div'); dropdown.classList.add('dropdown'); // create the dropdown toggle button const toggle = document.createElement('button'); toggle.classList.add('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'); - dropdown.appendChild(menu); + menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); // create the version list const versionList = document.createElement('ul'); versionList.classList.add('version-list'); menu.appendChild(versionList); // create the prev and next buttons const prevBtn = document.createElement('button'); prevBtn.classList.add('prev-btn'); prevBtn.textContent = '<'; const nextBtn = document.createElement('button'); nextBtn.classList.add('next-btn'); nextBtn.textContent = '>'; menu.appendChild(prevBtn); menu.appendChild(nextBtn); - // place dropdown next to btnSave - btnSave.parentNode.insertBefore(dropdown, btnSave.nextSibling); + // 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 { - position: fixed; right: 0; white-space: nowrap; } .hasversions .btnSave { - margin-right: 104px; + 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 { - z-index: 99999; - } .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: 100%; + top: 40px; right: 0; left: auto; margin-top: -2px; padding: 10px; border: solid 1px #444; background: #eee; border-radius: 5px 0px 0px 5px; } - .dropdown.open .dropdown-menu { + .dropdown-menu a { + color: #000; + } + + .dropdown-menu.open { display: block; - z-index: 3; + z-index: 4; } - .dropdown .dropdown-menu .menu-item { + .dropdown-menu .menu-item { padding: 2px; } - .dropdown .dropdown-menu .menu-item a { + .dropdown-menu .menu-item a { line-height: 20px; } - .dropdown .dropdown-menu .menu-item:hover { + .dropdown-menu .menu-item:hover { background: #29b6f2; color: #fff; margin-left: -2px; margin-right: 2px; } - .dropdown .dropdown-menu .menu-item:hover a { + .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'); + 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); - } + event.stopPropagation(); + + var versionList = document.querySelector('.version-list'); + var pagecount = parseInt(versionList.dataset.pagecount); - prevBtn.disabled = (currentPage <= 1); - nextBtn.disabled = (currentPage >= 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); - } + event.stopPropagation(); - prevBtn.disabled = (currentPage <= 1); - nextBtn.disabled = (currentPage >= pagecount); + 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'); - } + 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'); + + // 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/resources/sql/20230711.DiagramAddPHID.sql b/resources/sql/20230711.DiagramAddPHID.sql new file mode 100644 index 0000000..93bf347 --- /dev/null +++ b/resources/sql/20230711.DiagramAddPHID.sql @@ -0,0 +1,61 @@ +ALTER TABLE {$NAMESPACE}_diagram.diagram + ADD COLUMN phid varbinary(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_diagram.diagram + ADD COLUMN viewPolicy varbinary(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_diagram.diagram + ADD COLUMN editPolicy varbinary(64) NOT NULL; + +UPDATE {$NAMESPACE}_diagram.diagram + SET phid = CONCAT('PHID-DIAG-', LPAD(id, 20, '0')), + viewPolicy = 'users', + editPolicy = 'users'; +COMMIT; + + + +SET NAMES utf8 ; + +SET character_set_client = {$CHARSET} ; + +CREATE TABLE {$NAMESPACE}_diagram.edge ( + src varbinary(64) NOT NULL, + type int(10) unsigned NOT NULL, + dst varbinary(64) NOT NULL, + dateCreated int(10) unsigned NOT NULL, + seq int(10) unsigned NOT NULL, + dataID int(10) unsigned DEFAULT NULL, + PRIMARY KEY (src,type,dst), + UNIQUE KEY key_dst (dst,type,src), + KEY src (src,type,dateCreated,seq) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_diagram.edgedata ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + data longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_diagram.diagram_transaction ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + phid varbinary(64) NOT NULL, + authorPHID varbinary(64) NOT NULL, + objectPHID varbinary(64) NOT NULL, + viewPolicy varbinary(64) NOT NULL, + editPolicy varbinary(64) NOT NULL, + commentPHID varbinary(64) NULL, + commentVersion int(10) unsigned NOT NULL, + transactionType varchar(32) NOT NULL, + oldValue longtext NOT NULL, + newValue longtext NOT NULL, + contentSource longtext NOT NULL, + metadata longtext NOT NULL, + dateCreated int(10) unsigned NOT NULL, + dateModified int(10) unsigned NOT NULL, + + PRIMARY KEY (id), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +); + diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php index cfd40af..e9bd8a1 100644 --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -1,76 +1,80 @@ 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', + // other urls ".*" => "DiagramController", ) ); } } \ No newline at end of file diff --git a/src/conduit/DiagramSearchConduitAPIMethod.php b/src/conduit/DiagramSearchConduitAPIMethod.php new file mode 100644 index 0000000..67dcb21 --- /dev/null +++ b/src/conduit/DiagramSearchConduitAPIMethod.php @@ -0,0 +1,479 @@ + 'optional map', + 'attachments' => 'optional map', + 'order' => 'optional string', + 'before' => 'optional int', + 'after' => 'optional int', + 'limit' => 'optional int', + ); + } + + /** + * This method should return a string that describes the type of data that + * the method returns. + * The return type is used for documentation purposes and is displayed on the + * documentation page for the method to help users understand what kind of + * data they can expect to receive when calling the method. + */ + protected function defineReturnType() { + return 'list'; + } + + /** + * The execute method is the core of your Conduit API method and is where + * you implement the logic that performs the desired operation. + * + * The execute method should return the result of the operation in a + * format that can be serialized as JSON. + * The result will be sent back to the client as the response to the API + * request. + */ + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + $constraints = $request->getValue('constraints', array()); + $attachments = $request->getValue('attachments', array()); + $order = $request->getValue('order'); + $before = $request->getValue('before'); + $after = $request->getValue('after'); + $limit = $request->getValue('limit'); + + if (!$order) { + $order = "newest"; + } + + if (!$limit || $limit > 100 || $limit < 1) { + $limit = 100; // maximum limit + } + + $query = new PhabricatorDiagramVersionQuery(); + $query->setViewer($viewer); + + if (isset($constraints['ids'])) { + $query->withDiagramIDs($constraints['ids']); + } + + if (isset($constraints['createdStart'])) { + $query->withModifiedAfter($constraints['createdStart']); + } + + if (isset($constraints['createdEnd'])) { + $query->withModifiedBefore($constraints['createdEnd']); + } + + if ($order) { + $query->setOrder($order); + } + + // Create a new AphrontCursorPagerView object to paginate the results. + $pager = new AphrontCursorPagerView(); + $pager->setPageSize($limit); + if ($after) { + $pager->setAfterID($after); + } + if ($before) { + $pager->setBeforeID($before); + } + + // execute query + $diagrams = $query->executeWithCursorPager($pager); + + $results = array(); + foreach ($diagrams as $diagram) { + $result = array( + 'id' => $diagram->getID(), + 'phid' => $diagram->getPHID(), + 'fields' => array( + 'authorPHID' => $diagram->getAuthorPHID(), + 'byteSize' => $diagram->getByteSize(), + 'dataURI' => $diagram->getDataURI(), + 'dateModified' => $diagram->getDateModified(), + 'viewPolicy' => $diagram->getViewPolicy(), + 'editPolicy' => $diagram->getEditPolicy() + ) + ); + + if (isset($attachments['includeData']) && $attachments['includeData']) { + $result['fields']['data'] = $diagram->getBase64Data(); + } + + if (isset($attachments['includeVersion']) && $attachments['includeVersion']) { + $result['fields']['version'] = $diagram->getVersion(); + } + + $results[] = $result; + } + + return array( + 'data' => $results, + 'cursor' => array( + 'limit' => $pager->getPageSize(), + 'after' => $pager->getNextPageID(), + 'before' => $pager->getPrevPageID(), + 'order' => $order + ) + ); + } + + /** + * Descriptive text what this Conduit API method does + * This method should return a string that describes what the method does. + * The method description is used for documentation purposes and is + * displayed on the documentation page for the method to help users + * understand what the method does and how to use it. + */ + public function getMethodDescription() { + return pht('Search for diagrams.'); + } + + /** + * Generates custom documentation pages for this Conduit API method. + * This method should return an array of PhabricatorDocumentationBoxPage + * objects representing the custom documentation pages you want to add for + * the method. + * Each PhabricatorDocumentationBoxPage object represents a single + * documentation page and includes a title and content. + */ + public function newDocumentationPages(PhabricatorUser $viewer) { + $pages = array(); + + // create different chapters + $pages[] = $this->getDocumentationAttachments($viewer); + $pages[] = $this->getDocumentationObjectFields($viewer); + $pages[] = $this->getDocumentationConstraints($viewer); + $pages[] = $this->getDocumentationResultOrder($viewer); + $pages[] = $this->getDocumentationPagingAndLimits($viewer); + + return $pages; + } + + /** + * Creates the documentation chapter about Attachments + */ + public function getDocumentationAttachments(PhabricatorUser $viewer) { + // set title and content of 'Attachments' documentation box + $title = pht('Attachments'); + $content = pht(<< | Search for diagrams with specific IDs. +| createdStart | Created After | epoch | Search for diagrams created on or after a specific date. +| createdEnd | Created Before | epoch | Search for diagrams created on or before a specific date. +EOREMARKUP + ); + + // format content + $content = $this->newRemarkupDocumentationView($content); + + // create documentation box + $page = $this->newDocumentationBoxPage( + $viewer, + $title, + $content + ); + + // set icon and anchor of documentation box for navigation menu on the left + $page->setAnchor('constraints'); + $page->setIconIcon('fa-filter'); + + return $page; + } + + /** + * Creates the documentation chapter about Object Fields + */ + public function getDocumentationObjectFields(PhabricatorUser $viewer) { + // set title and content of 'Object Fields' documentation box + $title = pht('Object Fields'); + $content = pht(<<newRemarkupDocumentationView($content); + + // create documentation box + $page = $this->newDocumentationBoxPage( + $viewer, + $title, + $content + ); + + // set icon and anchor of documentation box for navigation menu on the left + $page->setAnchor('fields'); + $page->setIconIcon('fa-cube'); + + return $page; + } + + /** + * Creates the documentation chapter about Result Ordering + */ + public function getDocumentationPagingAndLimits(PhabricatorUser $viewer) { + // set title and content of 'Object Fields' documentation box + $title = pht('Paging and Limits'); + $content = pht(<<newRemarkupDocumentationView($content); + + // create documentation box + $page = $this->newDocumentationBoxPage( + $viewer, + $title, + $content + ); + + // set icon and anchor of documentation box for navigation menu on the left + $page->setAnchor('paging'); + $page->setIconIcon('fa-clone'); + + return $page; + } + + /** + * Creates the documentation chapter about Result Ordering + */ + public function getDocumentationResultOrder(PhabricatorUser $viewer) { + // set title and content of 'Object Fields' documentation box + $title = pht('Result Ordering'); + $content = pht(<<newRemarkupDocumentationView($content); + + // create documentation box + $page = $this->newDocumentationBoxPage( + $viewer, + $title, + $content + ); + + // set icon and anchor of documentation box for navigation menu on the left + $page->setAnchor('ordering'); + $page->setIconIcon('fa-sort-numeric-asc'); + + return $page; + } +} diff --git a/src/conduit/DiagramUploadConduitAPIMethod.php b/src/conduit/DiagramUploadConduitAPIMethod.php new file mode 100644 index 0000000..e8cfa86 --- /dev/null +++ b/src/conduit/DiagramUploadConduitAPIMethod.php @@ -0,0 +1,127 @@ + 'required nonempty base64-bytes', + 'id' => 'optional id of diagram to be updated', + 'viewPolicy' => 'optional valid policy string or ', + ); + } + + /** + * This method should return a string that describes the type of data that + * the method returns. + * The return type is used for documentation purposes and is displayed on the + * documentation page for the method to help users understand what kind of + * data they can expect to receive when calling the method. + */ + protected function defineReturnType() { + return 'nonempty guid'; + } + + /** + * The execute method is the core of your Conduit API method and is where + * you implement the logic that performs the desired operation. + * + * The execute method should return the result of the operation in a + * format that can be serialized as JSON. + * The result will be sent back to the client as the response to the API + * request. + */ + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + $base64_data = $request->getValue('data_base64'); + $diagram_id = $request->getValue('id'); + + 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_base64_data = base64_encode($data); + + if (DiagramController::equalPngMetaData($base64_data, $old_base64_data)) { + return array( + 'result' => $diagram->getPHID(), + 'id' => $diagram->getDiagramID(), + 'error_code' => null, + 'error_info' => null + ); + } + } + } + + // verify if the uploaded data really contains a drawio diagram + if (DiagramController::isDrawioPngBase64($base64_data) == false) { + return array( + 'result' => null, + 'id' => null, + 'error_code' => -1, + 'error_info' => 'invalid base64 data' + ); + } + + // 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()); + + return array( + 'result' => $diagram->getPHID(), + 'id' => $diagram->getDiagramID(), + 'error_code' => null, + 'error_info' => null + ); + } catch (Exception $e) { + // error occurred during saving + return array( + 'result' => null, + 'id' => null, + 'error_code' => $e->getCode(), + 'error_info' => $e->getMessage() + ); + } + } + + /** + * Descriptive text what this Conduit API method does + * This method should return a string that describes what the method does. + * The method description is used for documentation purposes and is + * displayed on the documentation page for the method to help users + * understand what the method does and how to use it. + */ + public function getMethodDescription() { + return pht('Upload a diagram to the server.'); + } +} diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index f068dcd..86234da 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,546 +1,571 @@ 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'); 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; } } $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/", $file); } else { $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 . '\/?/', 'data/', $file ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { $file = preg_replace('/^\/diagram\/(' . $diagramid . '\/)?/', 'data/', $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) { - $diagram = id(new DiagramVersion())->loadLatestByDiagramID($diagram_id); + $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( + $diagram_id); } else { - $diagram = id(new DiagramVersion())->loadByVersionedDiagramID($diagram_id, $version); + $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( + $diagram_id, $version); } - if ($diagram) { - $data = $diagram->getData(); + 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 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™ and Lucidchart™ files . ' + . '.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(), - 'var spinnerOpts = {' - . 'hwaccel: false,' - . 'length: 24,' - . 'radius: 12,' - . 'shadow: false,' - . 'speed: 1.5,' - . 'trail: 60,' - . 'width: 8};' - ) + )) )) )), phutil_tag( 'script', array( 'src' => 'phorge_extension.js' ), '' ), phutil_tag( 'script', array(), phutil_safe_html('loadJsExtension("' . $diagramName . '", "' + . $diagramPHID + . '", "' . $diagramVersion . '", "' . $diagramBase64 . '");') ), phutil_tag( 'div', array( 'class' => 'crumbs', 'style' => 'top:48px;' . 'margin-left: 4px;' . 'position: fixed;' . 'font-weight: bold;' ), array( phutil_tag( 'a', array( - 'href' => '.' + 'href' => $applicationUrl ), array( phutil_tag( 'span', array( 'class' => 'phui-font-fa fa-sitemap', 'style' => 'padding-right:5px;' )) )), phutil_tag( 'a', array( - 'href' => '.' + '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; } } diff --git a/src/editor/DiagramTransactionEditor.php b/src/editor/DiagramTransactionEditor.php new file mode 100644 index 0000000..0b62b3e --- /dev/null +++ b/src/editor/DiagramTransactionEditor.php @@ -0,0 +1,113 @@ +setMailReceiver($object); + } + + /** + * Returns the application class associated with the editor. + * This method is used to determine which application the editor belongs to, + * and can be used to customize the behavior of the editor based on the + * application it is associated with. + */ + public function getEditorApplicationClass() { + return DiagramApplication::class; + } + + /** + * Shows the application name at + * https:///settings/user/cat/page/emailpreferences/ + */ + public function getEditorObjectsDescription() { + return pht('Diagrams'); + } + + /** + * List of actions for which an email can be sent. + * These descriptions are listed at + * https:///settings/user/cat/page/emailpreferences/ + */ + public function getMailTagsMap() { + return array( + DiagramTransaction::MAILTAG_CONTENT => + pht("Someone changed a diagram's content."), + ); + } + + /** + * This method determines whether an email notification should + * be sent for the transaction. + */ + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + /** + * This method determines whether a feed story should be published + * under 'Recent Activities' for the transaction. + */ + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + /** + * This method returns a list of recipients for the email notification. + */ + protected function getMailTo(PhabricatorLiskDAO $object) { + return array( + $this->getActingAsPHID() + ); + } + + /** + * Generates a part of the email subject. + * An email's subject is formatted as follows: + * [prefix] [action] title + * This function represents the title + */ + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $subject = 'Diagram ' . $object->getID(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject($subject); + } + + /** + * Generates the email message content + */ + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + + $body->addLinkSection( + pht('DIAGRAM DETAIL'), + PhabricatorEnv::getProductionURI($object->getViewURI())); + + return $body; + } + + /** + * Generates a part of the email subject. + * An email's subject is formatted as follows: + * [prefix] [action] title + * This function represents the [prefix] + */ + protected function getMailSubjectPrefix() { + return '[Diagram]'; + } +} diff --git a/src/mail/DiagramReplyHandler.php b/src/mail/DiagramReplyHandler.php new file mode 100644 index 0000000..89a2341 --- /dev/null +++ b/src/mail/DiagramReplyHandler.php @@ -0,0 +1,27 @@ +withPHIDs($phids); + } + + /** + * Loads handles for objects of the PHID type + * In general, an implementation should call `setName()` and `setURI()` on + * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other + * handle properties. + */ + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $diagram = $objects[$phid]; + $handle->setName('DIAG' . $diagram->getID()); + $handle->setURI('/diagram/DIAG' . $diagram->getID()); + } + } + + /** + * Creates a new object of the PHID type. + */ + public function newObject() { + return new Diagram(); + } + + /** + * Returns the application class associated with the PHID type. + */ + public function getPHIDTypeApplicationClass() { + return DiagramApplication::class; + } + + /** + * Name of the PHID type + * Shown at https:///config/module/phid/ + */ + public function getTypeName() { + return pht('Diagram'); + } + +} diff --git a/src/query/PhabricatorDiagramQuery.php b/src/query/PhabricatorDiagramQuery.php index c3627c3..131f1a7 100644 --- a/src/query/PhabricatorDiagramQuery.php +++ b/src/query/PhabricatorDiagramQuery.php @@ -1,108 +1,50 @@ diagramIDs = $diagram_ids; - return $this; - } - - public function withIDs(array $diagram_ids) { - return $this->withDiagramIDs($diagram_ids); - } - - public function withModifiedAfter($datetime) { - $this->modifiedAfter = $datetime; - return $this; - } - - public function withModifiedBefore($datetime) { - $this->modifiedBefore = $datetime; + public function withPHIDs(array $phids) { + $this->phids = $phids; return $this; } protected function loadPage() { - $table = new DiagramVersion(); + $table = new Diagram(); $conn_r = $table->establishConnection('r'); - // we return a DiagramVersion object which has a different id - // than the one we mention in the Remarkup code. - // E.g. {DIAG1} may point to the 2nd version of the object. - // Diagram's id is 1, but DiagramVersion's id is 2. - // Because of this we abuse the id in the resultset a little bit $data = queryfx_all( $conn_r, 'SELECT * - FROM ( - SELECT result.diagramID AS id, /* abuse */ - result.diagramID, - result.version, - result.phid, - result.authorPHID, - result.dateCreated, - result.dateModified, - result.byteSize, - result.data, - result.viewPolicy, - result.editPolicy - FROM ( - SELECT data.* - FROM %T data - INNER JOIN ( - SELECT MAX(id) AS id, - diagramid - FROM %T - GROUP BY diagramid HAVING MAX(id) - ) filter - ON data.id = filter.id - ) result - ) r %Q %Q %Q', - $table->getTableName(), + FROM %T + %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); - if ($this->diagramIDs !== null) { - $where[] = qsprintf( - $conn_r, - 'diagramID IN (%Ld)', - $this->diagramIDs); - } - - if ($this->modifiedAfter !== null) { - $where[] = qsprintf( - $conn_r, - 'dateModified >= %d', - $this->modifiedAfter); - } - - if ($this->modifiedBefore !== null) { + if ($this->phids !== null) { $where[] = qsprintf( $conn_r, - 'dateModified <= %d', - $this->modifiedBefore); + 'phid IN (%Ls)', + $this->phids); } return $this->formatWhereClause($conn_r, $where); } public function getQueryApplicationClass() { return DiagramApplication::class; } } diff --git a/src/query/PhabricatorDiagramTransactionQuery.php b/src/query/PhabricatorDiagramTransactionQuery.php new file mode 100644 index 0000000..386806a --- /dev/null +++ b/src/query/PhabricatorDiagramTransactionQuery.php @@ -0,0 +1,10 @@ +diagramIDs = $diagram_ids; return $this; } public function withIDs(array $diagram_ids) { return $this->withDiagramIDs($diagram_ids); } public function withModifiedAfter($datetime) { $this->modifiedAfter = $datetime; return $this; } public function withModifiedBefore($datetime) { $this->modifiedBefore = $datetime; return $this; } protected function loadPage() { $table = new DiagramVersion(); $conn_r = $table->establishConnection('r'); // we return a DiagramVersion object which has a different id // than the one we mention in the Remarkup code. // E.g. {DIAG1} may point to the 2nd version of the object. // Diagram's id is 1, but DiagramVersion's id is 2. // Because of this we abuse the id in the resultset a little bit $data = queryfx_all( $conn_r, 'SELECT * FROM ( SELECT result.diagramID AS id, /* abuse */ result.diagramID, result.version, result.phid, result.authorPHID, result.dateCreated, result.dateModified, result.byteSize, result.data, result.viewPolicy, result.editPolicy FROM ( SELECT data.* FROM %T data INNER JOIN ( SELECT MAX(id) AS id, diagramid FROM %T GROUP BY diagramid HAVING MAX(id) ) filter ON data.id = filter.id ) result ) r %Q %Q %Q', $table->getTableName(), $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->diagramIDs !== null) { $where[] = qsprintf( $conn_r, 'diagramID IN (%Ld)', $this->diagramIDs); } if ($this->modifiedAfter !== null) { $where[] = qsprintf( $conn_r, 'dateModified >= %d', $this->modifiedAfter); } if ($this->modifiedBefore !== null) { $where[] = qsprintf( $conn_r, 'dateModified <= %d', $this->modifiedBefore); } return $this->formatWhereClause($conn_r, $where); } - + public function getQueryApplicationClass() { return DiagramApplication::class; } } diff --git a/src/remarkup/PhabricatorRemarkupDiagramRule.php b/src/remarkup/PhabricatorRemarkupDiagramRule.php index 45c8c90..efbf063 100644 --- a/src/remarkup/PhabricatorRemarkupDiagramRule.php +++ b/src/remarkup/PhabricatorRemarkupDiagramRule.php @@ -1,102 +1,102 @@ getEngine()->getConfig('viewer'); - $objects = id(new PhabricatorDiagramQuery()) + $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(), 'alt' => $alt, 'ondblclick' => 'window.open("/diagram/DIAG' . $diagram->getDiagramID() . '", "_blank")', ) ) ); return $output; } -} \ No newline at end of file +} diff --git a/src/storage/Diagram.php b/src/storage/Diagram.php index 0432782..d50b356 100644 --- a/src/storage/Diagram.php +++ b/src/storage/Diagram.php @@ -1,142 +1,250 @@ openTransaction(); $this->delete(); $this->saveTransaction(); } /** * Return an array of capabilities that this object type supports. * See PhabricatorPolicyCapability for a list of available capabilities. - * + * * Interface: PhabricatorPolicyInterface */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } + + /** + * Returns an instance of PhabricatorApplicationTransactionEditor. + * This class is responsible for applying transactions (which represent + * changes to an object) to an object. It handles the process of validating, + * applying, and publishing transactions. + * + * Interface: PhabricatorApplicationTransactionInterface + */ + public function getApplicationTransactionEditor() { + return new DiagramTransactionEditor(); + } + + /** + * Returns an instance of a subclass of PhabricatorApplicationTransaction. + * This class represents a single change to an object, such as setting a + * new value for a property or adding a comment. + * The template is used to create new transactions of the appropriate type + * for the object. + * + * Interface: PhabricatorApplicationTransactionInterface + */ + public function getApplicationTransactionTemplate() { + return new DiagramTransaction(); + } + /** * Configures application-wide storage settings. * This creates a mapping of the corresponding database table. */ public function getConfiguration() { return array( + self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'id' => 'auto', + 'viewPolicy' => 'policy', + 'editPolicy' => 'policy', ), self::CONFIG_KEY_SCHEMA => array( + 'key_phid' => null, 'PRIMARY' => array( 'columns' => array('id'), 'unique' => true, ), ), ); } + /** + * This method is required for the subcription application. + * It's used for storing the old version of the object. + * This extension works a bit different and stores the older + * versions in another table (diagram_versions). + * So the result of this method is not important here. + */ + public function getContent() { + return $this->getID(); + } + + /** + * This method is required for the subcription application. + */ + public function getCreatorPHID() { + $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( + $this->getID()); + return $diagramVersion->getAuthorPHID(); + } + /** * Return a unique identifier for an object. * In Phabricator, a monogram is a unique identifier for an object, such * as a task or event. * For example, Maniphest tasks are identified with monograms like "T123". */ public function getMonogram() { return 'DIAG'.$this->getID(); } + /** + * Returns the number of versions + */ + public function getNumberOfVersions() { + $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( + $this->getID()); + return $diagramVersion->getVersion(); + } + /** * Return a string that uniquely identifies the PHID type for this object * type. This is used by the PHID system to generate and manage PHIDs for * this object type. */ public function getPHIDType() { return 'DIAG'; } /** * Return the policy for the given capability. * * Interface: PhabricatorPolicyInterface */ public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; default: return PhabricatorPolicies::POLICY_NOONE; } } + /** + * Return the URL which links to the diagram + */ + public function getViewURI() { + if (!$this->getPHID()) { + throw new Exception( + pht('You must save a diagram before you can generate a view URI.') + ); + } + + $uri = '/diagram/DIAG' + .$this->getID(); + + return $uri; + } + /** * Return true if the given user has the given capability automatically, * without needing to check the object's policy. For example, you might * return true here if the user is an administrator or if they own the * object. * * Interface: PhabricatorPolicyInterface */ public function hasAutomaticCapability( $capability, PhabricatorUser $viewer ) { return false; } /** * Return true if the given user is automatically subscribed to this * object. For example, you might return true here if the user is the * author of the object or if they are mentioned in the object's content. */ public function isAutomaticallySubscribed($phid) { - return true; + return false; } /** * Create a new record in the diagram table and returns the generated record */ public function createNewDiagram() { $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $record = array( // no other columns defined but the primary key (id) ); + if (!$this->getPHID()) { + $this->phid = $this->generatePHID(); + } + + $record = array( + 'phid' => $this->getPHID(), + 'viewPolicy' => $this->getViewPolicy(), + 'editPolicy' => $this->getEditPolicy() + ); + queryfx( $conn_w, - 'INSERT INTO %T () VALUES ()', - $table_name - ); + 'INSERT INTO %T (%Q) VALUES (%Ls)', + $table_name, + implode(', ', array_keys($record)), + array_values($record)); $this->id = $conn_w->getInsertID(); return $this; } + + + /** + * Returns Diagram object for a given ID + */ + public function loadByID($diagramID) { + if (is_object($diagramID)) { + $diagramID = (string)$diagramID; + } + + if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { + return null; + } + + return $this->loadOneWhere( + 'ID = %d', + $diagramID); + } } \ No newline at end of file diff --git a/src/storage/DiagramSchemaSpec.php b/src/storage/DiagramSchemaSpec.php new file mode 100644 index 0000000..61a4622 --- /dev/null +++ b/src/storage/DiagramSchemaSpec.php @@ -0,0 +1,9 @@ +buildEdgeSchemata(new Diagram()); + } + +} diff --git a/src/storage/DiagramTransaction.php b/src/storage/DiagramTransaction.php new file mode 100644 index 0000000..7c6436e --- /dev/null +++ b/src/storage/DiagramTransaction.php @@ -0,0 +1,127 @@ +getAuthorPHID(); + $author_handle = $this->getHandle($author_phid); + $diagram = $this->getObject(); + $first_version = $diagram->getNumberOfVersions() == 1; + + $author_link = phutil_tag( + 'a', + array( + 'href' => $author_handle->getURI(), + 'class' => 'phui-handle phui-link-person', + ), + $author_handle->getName() + ); + + $diagram_link = phutil_tag( + 'a', + array( + 'href' => $diagram->getViewURI(), + ), + $diagram->getMonogram() + ); + + if ($this->transactionType == "core:subscribers") { + // subscription changed + if (empty($this->newValue)) { + return pht('%s unsubscribed from %s %s.', + $author_link, + $diagram->getApplicationName(), + $diagram_link); + } else { + return pht('%s subscribed to %s %s.', + $author_link, + $diagram->getApplicationName(), + $diagram_link); + } + } else { + if ($this->transactionType == "content") { + // content changed + if ($first_version) { + return pht('%s created %s %s.', + $author_link, + $diagram->getApplicationName(), + $diagram_link); + } else { + return pht('%s edited %s %s.', + $author_link, + $diagram->getApplicationName(), + $diagram_link); + } + } + } + + // some unknown action was executed + return pht('%s did something to %s %s.', + $author_link, + $diagram->getApplicationName(), + $diagram_link); + } + + /** + * This method determines whether the transaction should be hidden in email + * notifications. + */ + public function shouldHideForMail(array $xactions) { + return false; + } + +} \ No newline at end of file diff --git a/src/storage/DiagramVersion.php b/src/storage/DiagramVersion.php index e896141..bf7dde3 100644 --- a/src/storage/DiagramVersion.php +++ b/src/storage/DiagramVersion.php @@ -1,371 +1,409 @@ data === null) { return null; } return base64_encode($this->data); } /** * returns the URL which links to the diagram PNG data */ public function getDataURI() { return PhabricatorEnv::getCDNURI( '/diagram/data/' .$this->getPHID()); } /** * Return an array of capabilities that this object type supports. * See PhabricatorPolicyCapability for a list of available capabilities. * * Interface: PhabricatorPolicyInterface */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } /** * Return the policy for the given capability. * * Interface: PhabricatorPolicyInterface */ public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; default: return PhabricatorPolicies::POLICY_NOONE; } } /** * Return the URL which links to the diagram PNG image */ public function getViewURI() { if (!$this->getPHID()) { throw new Exception( pht('You must save a diagram before you can generate a view URI.') ); } $uri = '/diagram/data/' .$this->getPHID(); return $uri; } /** * Configures application-wide storage settings. * This creates a mapping of the corresponding database table. */ public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'diagramID' => 'uint32', 'version' => 'uint32', 'authorPHID' => 'phid', 'byteSize' => 'uint64', 'data' => 'bytes', 'viewPolicy' => 'policy', 'editPolicy' => 'policy', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'key_diagramID_version' => array( 'columns' => array('diagramID', 'version'), 'unique' => true, ), 'key_authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Return the name of the database table that is represented by this class */ public function getTableName() { return 'diagram_version'; } /** * Return a string that uniquely identifies the PHID type for this object * type. This is used by the PHID system to generate and manage PHIDs for * this object type. */ public function getPHIDType() { return 'DGVN'; } /** * Return true if the given user has the given capability automatically, * without needing to check the object's policy. For example, you might * return true here if the user is an administrator or if they own the * object. * * Interface: PhabricatorPolicyInterface */ public function hasAutomaticCapability( $capability, PhabricatorUser $viewer) { return false; } /** * Creates and initializes a new DiagramVersion object */ public static function initializeNewDiagram(PhabricatorUser $actor) { return id(new self()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setVersion(1) ->setDateCreated(time()) ->setDateModified(time()); } /** * Creates a new DiagramVersion object and loads the given base64 data in it */ public static function newFromFileData( $base64_data, array $params = array()) { $actor = idx($params, 'actor'); if (!$actor) { throw new Exception(pht('Missing required actor for new file data.')); } $diagramID = idx($params, 'diagramID'); if (!is_numeric($diagramID)) { $diagramID = null; } $data = base64_decode($base64_data); $diagram = self::initializeNewDiagram($actor); $diagram->setByteSize(strlen($data)); $diagram->setData($data); $diagram->setDiagramID($diagramID); $diagram->save(); return $diagram; } /** * Permanently destroy this object. This is used by the destructible * interface to allow administrators to permanently delete objects from * the system. */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /** * Returns an ID which can be used for a newly created Diagram object */ public function generateDiagramID() { $conn_r = $this->establishConnection('r'); $table_name = $this->getTableName(); $max_diagram_id = queryfx_one( $conn_r, 'SELECT MAX(diagramID) max_diagram_id FROM %T', $table_name)['max_diagram_id']; return (int)$max_diagram_id + 1; } /** * Returns all DiagramVersion objects for a given diagram */ public function loadByDiagramID($diagramID) { if (is_object($diagramID)) { $diagramID = (string)$diagramID; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } return $this->loadAllWhere( 'diagramID = %d ORDER BY version DESC', $diagramID); } /** * Returns the latest version of the given diagram */ public function loadLatestByDiagramID($diagramID) { if (is_object($diagramID)) { $diagramID = (string)$diagramID; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } return $this->loadOneWhere( 'diagramID = %d ORDER BY version DESC LIMIT 1', $diagramID); } /** * Returns a specific DiagramVersion object */ public function loadByDiagramPHID($diagramVersionPHID) { if (is_object($diagramVersionPHID)) { $diagramVersionPHID = (string)$diagramVersionPHID; } return $this->loadOneWhere( 'phid = %s ORDER BY version DESC LIMIT 1', $diagramVersionPHID); } /** * Returns a specific DiagramVersion object for a given diagram and * version number */ public function loadByVersionedDiagramID($diagramID, $version) { if (is_object($diagramID)) { $diagramID = (string)$diagramID; } if (is_object($version)) { $version = (string)$version; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } if (!$version || (!is_int($version) && !ctype_digit($version))) { return null; } return $this->loadOneWhere( 'diagramID = %d AND version = %d', $diagramID, $version); } + /** + * Publishes a modification of a diagram via email + */ + public function publishNewVersion($request, $diagram_id) { + $xactions[] = id(new DiagramTransaction()) + ->setTransactionType(DiagramContentTransaction::TRANSACTIONTYPE) + ->setNewValue(array('=' => $diagram_id)); + + if ($request instanceof ConduitAPIRequest) { + // Handle ConduitAPIRequest + $viewer = $request->getUser(); + + // Set the content source for the transaction editor + $content_source = PhabricatorContentSource::newForSource( + PhabricatorConduitContentSource::SOURCECONST, + array( + 'params' => $request->getAllParameters() + )); + $editor = id(new DiagramTransactionEditor()) + ->setActor($viewer) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true); + } elseif ($request instanceof AphrontRequest) { + // Handle AphrontRequest + $viewer = $request->getViewer(); + $editor = id(new DiagramTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + } else { + // Handle other types of requests + throw new Exception('Unsupported request type'); + } + + $diagram = id(new Diagram())->loadByID($diagram_id); + $editor->applyTransactions($diagram, $xactions); + } + /** * Stores a new diagram (version) */ public function save() { // Load the last record with the same PHID. $last_record = null; if ($this->getDiagramID() !== null) { - $last_record = id(new PhabricatorDiagramQuery()) + $last_record = id(new PhabricatorDiagramVersionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDiagramIDs(array($this->getDiagramID())) ->setLimit(1) ->executeOne(); } if ($last_record === null) { // If there is no last record, this is a new diagram object. $this->setVersion(1); $newDiagram = new Diagram(); $newDiagram->createNewDiagram(); $this->setDiagramID($newDiagram->getID()); } else { // If there is a last record, this is a new version of an existing // diagram object. $this->setVersion($last_record->getVersion() + 1); $this->setDateCreated($last_record->getDateCreated()); } // Check if a row with the same PHID and version already exists $existing_record = null; if ($this->getPHID() !== null && $this->getVersion() !== null) { - $existing_record = id(new PhabricatorDiagramQuery()) + $existing_record = id(new PhabricatorDiagramVersionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->withWhere( array( array('version', '=', $this->getVersion()), )) ->setLimit(1) ->executeOne(); } if ($existing_record === null) { // If there is no existing record, create a new row in the table. $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $this->phid = $this->generatePHID(); if ($this->diagramID === null) { $this->diagramID = $this->generateDiagramID(); } $record = array( 'phid' => $this->getPHID(), 'diagramID' => $this->getDiagramID(), 'version' => $this->getVersion(), 'authorPHID' => $this->getAuthorPHID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'byteSize' => $this->getByteSize(), 'viewPolicy' => $this->getViewPolicy(), 'editPolicy' => $this->getEditPolicy(), 'data' => $this->getData(), ); if ($this->getID() !== null) { // If the ID property is set, include it in the data to insert. $record['id'] = $this->getID(); } queryfx( $conn_w, 'INSERT INTO %T (%Q) VALUES (%Ls, %B)', $table_name, implode(', ', array_keys($record)), array_values(array_slice($record, 0, -1)), end($record)); $this->id = $conn_w->getInsertID(); } else { // If there is an existing record, throw an exception. throw new Exception( pht('A diagram with PHID "%s" and version "%s" already exists.', $this->getPHID(), $this->getVersion()) ); } return $this; } } diff --git a/src/xaction/DiagramContentTransaction.php b/src/xaction/DiagramContentTransaction.php new file mode 100644 index 0000000..c1bf2d0 --- /dev/null +++ b/src/xaction/DiagramContentTransaction.php @@ -0,0 +1,8 @@ +getID(); + } + +} \ No newline at end of file