Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2991962
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
105 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R5 Diagrams
Attached
Detach File
Event Timeline
Log In to Comment