Page MenuHomePhorge

No OneTemporary

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 @@
<?php
final class DiagramApplication extends PhabricatorApplication {
public function getName() {
return pht('Diagrams');
}
public function getBaseURI() {
return '/diagram/';
}
public function getIcon() {
return 'fa-sitemap';
}
public function getShortDescription() {
return pht("Editor for flowcharts, process diagrams, org charts, "
. "UML, ER and network diagrams");
}
public function getTitleGlyph() {
return '\xE2\x98\x85';
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRemarkupRules() {
return array(
new PhabricatorRemarkupDiagramRule(),
);
}
public function getRoutes() {
return array(
'/diagram/' => array(
// url to image data
"data/(?P<diagramphid>PHID-DGVN-[a-z0-9]{20})"
. "(?:/(?P<version>[^/]+))?"
. "(?:/|\?|$).*"
=> "DiagramController",
// version info requests
"version/DIAG(?P<versioninfodiagram>(\d+))"
. "/(?P<versioninfopage>(\d+))"
. "(?:/|\?|$).*"
=> "DiagramController",
// draw.io iframe urls
"(?:(?P<versioneddiagramid>DIAG(\d+))/(?P<version>\d+)/?)"
. "(?P<route>iframe)"
. "(?:/|\?|$).*"
=> "DiagramController",
// draw.io iframe urls
"(?:(?P<diagramid>DIAG(\d+))/?)?"
. "(?P<route>iframe)"
. "(?:/|\?|$).*"
=> "DiagramController",
// url with diagram id and version in it
"(?P<versioneddiagramid>DIAG(\d+))"
. "/(?P<version>\d+)"
. "(?:/|\?|$).*"
=> "DiagramController",
// url with diagram id in it
"(?P<diagramid>DIAG(\d+))"
- . "(?:/|\?|$).*"
+ . "(?:/|\?|$).*"
=> "DiagramController",
-
+
+ // url for validating subscription
+ "subscribed/request/"
+ . "(?P<subscriptionphid>[^/]+)/" => '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 @@
+<?php
+
+final class DiagramSearchConduitAPIMethod extends ConduitAPIMethod {
+
+ /**
+ * Name of the Conduit API method.
+ * This method should return a string that represents the name of the method.
+ * The method name is used to identify the method when calling it via the
+ * Conduit API.
+ */
+ public function getAPIMethodName() {
+ return 'diagram.search';
+ }
+
+ /**
+ * Defines the input parameters for this API.
+ * This method should return an associative array where the keys are
+ * the names of the parameters and the values are strings that describe
+ * the types of the parameters.
+ * The parameter types are used for documentation purposes and are displayed
+ * on the documentation page for the method to help users understand what
+ * kind of data they can pass to the method when calling it.
+ */
+ protected function defineParamTypes() {
+ return array(
+ 'constraints' => 'optional map<string, wild>',
+ 'attachments' => 'optional map<string, bool>',
+ '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<dict>';
+ }
+
+ /**
+ * 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(<<<EOREMARKUP
+By default, only basic information about objects is returned.
+If you want more extensive information, you can use available
+attachments to get more information in the results (like includeData).
+
+Generally, requesting more information means the query executes more
+slowly and returns more data (in some cases, much more data).
+You should normally request only the data you need.
+
+To request extra data, specify which attachments you want in the attachments parameter:
+``` lang=json, name=Example Attachments Request
+{
+ ...
+ "attachments": {
+ "includeData": true
+ },
+ ...
+}
+```
+
+This example specifies that results should include the base64-formatted
+content of the diagrams. In the return value, each object will now have
+this information filled out in the corresponding attachments value:
+``` lang=json, name=Example Attachments Result
+{
+ "0": {
+ "id": "7",
+ "phid": "PHID-DGVN-hubmg6vge3srelgo3jj6",
+ "fields": {
+ "authorPHID": "PHID-USER-hmlbd2wfegzjhbpbnoc7",
+ "byteSize": "3152",
+ "dataURI": "http://phorge.local/diagram/data/PHID-DGVN-hubmg6vge3srelgo3jj6",
+ "dateModified": "1688493034",hubmg6vge3srelgo3jj6
+ "viewPolicy": "users",
+ "editPolicy": "PHID-USER-hmlbd2wfegzjhbpbnoc7",
+ "data": "iVBORw0KGg...K5CYII="
+ }
+ },
+ ...
+}
+```
+
+These are the available fields:
+| Key | Type | Description
+| -------------- | --------------- | ---
+| includeData | Include Data | Include the image content of the diagrams in the search results.
+| includeVersion | Include Version | Include the version number of the diagrams in the search results.
+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('attachments');
+ $page->setIconIcon('fa-cubes');
+
+ return $page;
+ }
+
+ /**
+ * Creates the documentation chapter about Constraints
+ */
+ public function getDocumentationConstraints(PhabricatorUser $viewer) {
+ // set title and content of 'Constraints' documentation box
+ $title = pht('Constraints');
+ $content = pht(<<<EOREMARKUP
+You can apply custom constraints by passing a dictionary in constraints.
+This will let you search for specific sets of diagrams (for example, you
+may want show only diagrams within a certain period).
+
+``` lang=json,name=Example Custom Constraints
+{
+ ...
+ "constraints": {
+ "ids": [1, 2, 5],
+ "createdStart": 1688493034,
+ ...
+ },
+ ...
+}
+```
+
+This method supports the following constraints:
+
+| Key | Label | Type | Description
+| ------------ | -------------- | --------- | ---
+| ids | IDs | list<int> | 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(<<<EOREMARKUP
+Diagrams matching your query are returned as a list of dictionaries in the
+data property of the results. Each dictionary has some metadata and a
+fields key, which contains the information about the diagram that most
+callers will be interested in.
+
+For example, the results may look something like this:
+``` lang=json, name=Example Attachments Result
+{
+ "0": {
+ "id": "7",
+ "phid": "PHID-DGVN-hubmg6vge3srelgo3jj6",
+ "fields": {
+ "authorPHID": "PHID-USER-hmlbd2wfegzjhbpbnoc7",
+ "byteSize": "3152",
+ "dataURI": "http://phorge.local/diagram/data/PHID-DGVN-hubmg6vge3srelgo3jj6",
+ "dateModified": "1688493034",
+ "viewPolicy": "users",
+ "editPolicy": "PHID-USER-hmlbd2wfegzjhbpbnoc7",
+ }
+ },
+ ...
+}
+```
+
+These are the available fields:
+| Key | Type | Description
+| -------------- | --------------- | ---
+| authorPHID | phid | PHID of the author of the diagram
+| byteSize | int | Size of the diagram in bytes
+| data | string | Base64 formatted content of diagram image
+| dataURI | uri | URL to diagram image
+| dateModified | int | Timestamp when diagram was last modified
+| version | int | Version number of diagram
+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('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(<<<EOREMARKUP
+Queries are limited to returning 100 results at a time.
+If you want fewer results than this, you can use limit to specify
+a smaller limit.
+
+If you want more results, you'll need to make additional queries to
+retrieve more pages of results.
+
+The result structure contains a cursor key with information you'll need
+in order to fetch the next page of results.
+After an initial query, it will usually look something like this:
+``` lang=json, name=Example Cursor Result
+{
+ ...
+ "cursor": {
+ "limit": 100,
+ "after": "1234",
+ "before": null,
+ "order": null
+ }
+ ...
+}
+```
+The limit and order fields are describing the effective limit and order
+the query was executed with, and are usually not of much interest.
+The after and before fields give you cursors which you can pass when
+making another API call in order to get the next (or previous) page of
+results.
+
+To get the next page of results, repeat your API call with all the same
+parameters as the original call, but pass the after cursor you received
+from the first call in the after parameter when making the second call.
+
+If you do things correctly, you should get the second page of results,
+and a cursor structure like this:
+``` lang=json, name=Second Result Page
+{
+ ...
+ "cursor": {
+ "limit": 5,
+ "after": "4567",
+ "before": "7890",
+ "order": null
+ }
+ ...
+}
+```
+You can now continue to the third page of results by passing the new after
+cursor to the after parameter in your third call, or return to the previous
+page of results by passing the before cursor to the before parameter.
+This might be useful if you are rendering a web UI for a user and want to
+provide "Next Page" and "Previous Page" links.
+
+If after is null, there is no next page of results available.
+Likewise, if before is null, there are no previous results available.
+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('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(<<<EOREMARKUP
+Use `order` to choose an ordering for the results.
+
+Choose a builtin order from the table below and specify it like this:
+``` lang=json, name=Choosing a Result Order
+{
+ ...
+ "order": "newest",
+ ...
+}
+```
+
+These builtin orders are available:
+| Key | Description
+| ------ | ---
+| newest | Creation (Newest First)
+| oldest | Creation (Oldest First)
+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('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 @@
+<?php
+
+final class DiagramUploadConduitAPIMethod extends ConduitAPIMethod {
+
+ /**
+ * Name of the Conduit API method.
+ * This method should return a string that represents the name of the method.
+ * The method name is used to identify the method when calling it via the
+ * Conduit API.
+ */
+ public function getAPIMethodName() {
+ return 'diagram.upload';
+ }
+
+ /**
+ * Defines the input parameters for this API.
+ * This method should return an associative array where the keys are
+ * the names of the parameters and the values are strings that describe
+ * the types of the parameters.
+ * The parameter types are used for documentation purposes and are displayed
+ * on the documentation page for the method to help users understand what
+ * kind of data they can pass to the method when calling it.
+ */
+ protected function defineParamTypes() {
+ return array(
+ 'data_base64' => 'required nonempty base64-bytes',
+ 'id' => 'optional id of diagram to be updated',
+ 'viewPolicy' => 'optional valid policy string or <phid>',
+ );
+ }
+
+ /**
+ * 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 @@
<?php
final class DiagramController extends PhabricatorController {
/**
* Processes incoming HTTP requests from Diagram application
*/
public function handleRequest(AphrontRequest $request) {
$this->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:
* <mxfile host="..." modified="..." agent="..." etag="..." version="..."...>
* <diagram id="O253AQcHVgjdl_wygdBA" name="Page-1">
* <mxGraphModel ...>
* <root>
* ...
* </mxCell>
* </root>
* </mxGraphModel>
* </diagram>
* </mxfile>
*
* 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 @@
+<?php
+
+final class DiagramTransactionEditor
+extends PhabricatorApplicationTransactionEditor {
+
+ /**
+ * Builds a reply handler for the transaction.
+ * A reply handler is an object that handles email replies to notifications
+ * sent by Phorge.
+ */
+ protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+ return id(new DiagramReplyHandler())
+ ->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://<phorge>/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://<phorge>/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 @@
+<?php
+
+final class DiagramReplyHandler
+ extends PhabricatorApplicationTransactionReplyHandler {
+
+ /**
+ * Returns the prefix for the object associated with the transaction.
+ * The prefix is used to identify the object in email notifications
+ * and other places within Phorge
+ */
+ public function getObjectPrefix() {
+ return DiagramPHIDType::TYPECONST;
+ }
+
+ /**
+ * Validate the recipient of an email notification.
+ * It checks whether the recipient is a valid user and whether they have
+ * permission to view the object associated with the notification.
+ */
+ public function validateMailReceiver($mail_receiver) {
+ if (!($mail_receiver instanceof Diagram)) {
+ throw new Exception(
+ pht('Mail receiver is not a %s!', 'Diagram'));
+ }
+ }
+
+}
diff --git a/src/phid/DiagramPHIDType.php b/src/phid/DiagramPHIDType.php
new file mode 100644
index 0000000..eb92249
--- /dev/null
+++ b/src/phid/DiagramPHIDType.php
@@ -0,0 +1,59 @@
+<?php
+
+final class DiagramPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'DIAG';
+
+ /**
+ * Builds a query to load objects of the PHID type.
+ */
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorDiagramQuery())
+ ->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://<phorge>/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 @@
<?php
final class PhabricatorDiagramQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
- protected $diagramIDs;
- protected $modifiedAfter;
- protected $modifiedBefore;
+ protected $phids;
- public function withDiagramIDs(array $diagram_ids) {
- $this->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 @@
+<?php
+
+final class PhabricatorDiagramTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new DiagramTransaction();
+ }
+
+}
\ No newline at end of file
diff --git a/src/query/PhabricatorDiagramQuery.php b/src/query/PhabricatorDiagramVersionQuery.php
similarity index 98%
copy from src/query/PhabricatorDiagramQuery.php
copy to src/query/PhabricatorDiagramVersionQuery.php
index c3627c3..0e3742f 100644
--- a/src/query/PhabricatorDiagramQuery.php
+++ b/src/query/PhabricatorDiagramVersionQuery.php
@@ -1,108 +1,108 @@
<?php
-final class PhabricatorDiagramQuery
+final class PhabricatorDiagramVersionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
protected $diagramIDs;
protected $modifiedAfter;
protected $modifiedBefore;
public function withDiagramIDs(array $diagram_ids) {
$this->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 @@
<?php
final class PhabricatorRemarkupDiagramRule
extends PhabricatorObjectRemarkupRule {
protected function getObjectNamePrefix() {
return 'DIAG';
}
public function getRuleVersion() {
return '1.0';
}
protected function loadObjects(array $ids) {
$viewer = $this->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 @@
<?php
final class Diagram extends DiagramDAO implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorDestructibleInterface,
PhabricatorPolicyInterface,
- PhabricatorSubscribableInterface,
- PhabricatorDestructibleInterface
+ PhabricatorSubscribableInterface
{
/**
* List of properties mapped to database table columns
*/
protected $id;
+ protected $phid;
+ protected $viewPolicy;
+ protected $editPolicy;
/**
* Permanently destroy this object. This is used by the destructible
* interface to allow administrators to permanently delete objects from
* the system.
*
* Interface: PhabricatorDestructibleInterface
*/
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine
) {
$this->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 @@
+<?php
+
+final class DiagramSchemaSpec extends PhabricatorConfigSchemaSpec {
+
+ public function buildSchemata() {
+ $this->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 @@
+<?php
+
+final class DiagramTransaction
+ extends PhabricatorModularTransaction {
+
+ const MAILTAG_CONTENT = 'diagram-content';
+
+ /**
+ * This method returns the name of the application associated with
+ * the transaction.
+ * It is used to determine which application the transaction belongs
+ * to and also what the name of the corresponding database is
+ */
+ public function getApplicationName() {
+ return 'diagram';
+ }
+
+ /**
+ * This method returns the type of the transaction.
+ * It is used to determine the type of transaction that is being performed
+ */
+ public function getApplicationTransactionType() {
+ return DiagramPHIDType::TYPECONST;
+ }
+
+ /**
+ * This method returns the base class for the transaction.
+ * It is used to determine the base class that the transaction extends.
+ */
+ public function getBaseTransactionClass() {
+ return DiagramTransactionType::class;
+ }
+
+ /**
+ * This method returns a list of tags associated with the email notification
+ * for the transaction.
+ * It is used to determine which tags should be associated with the email
+ * notification for a given transaction.
+ */
+ public function getMailTags() {
+ $tags = array();
+ return $tags;
+ }
+
+ /**
+ * This method returns a list of PHIDs that are required to handle the
+ * transaction.
+ * It is used to determine which objects are required to handle the
+ * transaction
+ */
+ public function getRequiredHandlePHIDs() {
+ $phids = parent::getRequiredHandlePHIDs();
+ return $phids;
+ }
+
+ /**
+ * Shows the text in the 'Recent Activity' for a Diagram modification event
+ */
+ public function getTitle() {
+ $author_phid = $this->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 @@
<?php
final class DiagramVersion extends DiagramDAO implements
PhabricatorDestructibleInterface,
PhabricatorPolicyInterface {
/**
* List of properties mapped to database table columns
*/
protected $phid;
protected $diagramID;
protected $version;
protected $authorPHID;
protected $byteSize;
protected $data;
protected $viewPolicy;
protected $editPolicy;
/**
* returns base64 of $data
*/
public function getBase64Data() {
if ($this->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 @@
+<?php
+
+final class DiagramContentTransaction
+ extends DiagramTransactionType {
+
+ const TRANSACTIONTYPE = 'content';
+}
+
diff --git a/src/xaction/DiagramTransactionType.php b/src/xaction/DiagramTransactionType.php
new file mode 100644
index 0000000..dbedeb8
--- /dev/null
+++ b/src/xaction/DiagramTransactionType.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class DiagramTransactionType
+extends PhabricatorModularTransactionType {
+
+ public function generateOldValue($object) {
+ return $object->getID();
+ }
+
+}
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 23, 04:33 (1 d, 10 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1140847
Default Alt Text
(105 KB)

Event Timeline