Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2895609
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
71 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff6a1fa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/data/drawio
+
+/src/__phutil_library_init__.php
+/src/__phutil_library_map__.php
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8b26de0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+draw.io integration in phorge
+=============================
+
+Installation
+============
+1) Extract the content of this repository into <phorge>/src/extensions
+2) <arcanist>/bin/arc liberate
+3) <phorge>/bin/storage upgrade
+4) CD to <phorge>/src/extensions/drawio/data
+5) git clone https://github.com/jgraph/drawio.git
+6) Diagrams application is available under "More Applications" in Phorge.
+ You may add it to your navigator menu via "Edit Menu"
+
+You may need to set the following rule in your httpd.conf for your Phorge's VirtualHost:
+
+ Header set Content-Security-Policy "default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline';"
+
+
+Usage
+=====
+When you start the Diagrams application, you will see the embedded version of [https://app.diagrams.net/](draw.io).
+You can reference the diagrams in your wiki pages or maniphest tasks by means of the DIAG token.
+E.g. {DIAG123}
+When you doubleclick on a diagram, the editor will open the corresponding diagram in new browser tab.
+
+You can create diagrams with multiple pages, but only the first one will be visualized.
+
+When you modify an existing diagram, a new version will be created.
+You can select older versions in the editor by means of the dropdown in the topright corner.
+If you don't see a dropdown, your diagram has only 1 version.
+
+Extra info
+==========
+[https://github.com/jgraph/drawio-integration]
+
diff --git a/data/phorge_extension.js b/data/phorge_extension.js
new file mode 100644
index 0000000..d2e417b
--- /dev/null
+++ b/data/phorge_extension.js
@@ -0,0 +1,429 @@
+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";
+ image.style.display = 'none';
+
+ var receive = function (evt) {
+ if (evt.data.length > 0) {
+ var msg = JSON.parse(evt.data);
+ if (msg.event == 'init') {
+ if (phorge_extension.diagramBase64) {
+ image.src = 'data:image/png;base64,' + phorge_extension.diagramBase64;
+
+ var diagramName = document.querySelector('.diagramName');
+ diagramName.style.display = 'inline-block';
+ diagramName.querySelector('a').innerText = phorge_extension.diagramName;
+ if (phorge_extension.diagramVersion != '') {
+ diagramName.querySelector('.version').innerText = '(#'
+ + phorge_extension.diagramVersion
+ + ')';
+ }
+ }
+
+ iframe.contentWindow.postMessage(JSON.stringify({
+ action: 'load',
+ autosave: 1,
+ xmlpng: image.getAttribute('src')
+ }), '*');
+ }
+ else if (msg.event == 'load') {
+ // enable Mathematical Typesettings by default
+ iframe.contentWindow.sb.editorUi.setMathEnabled(true);
+
+ copySettingsToIframe();
+
+ setupButtonsInMenuToolbar();
+
+ loadingtext.style.display = 'none';
+ }
+ else if (msg.event == 'export') {
+ saveFlowchart(name, msg.data, iframe);
+ }
+ else if (msg.event == 'exit') {
+ var originURL = sessionStorage['originURL'];
+ if (originURL) {
+ sessionStorage.removeItem('originURL');
+ document.location.href = originURL;
+ }
+ }
+ else if (msg.event == 'save') {
+ iframe.contentWindow.postMessage(JSON.stringify({
+ action: 'export',
+ format: 'xmlpng',
+ xml: msg.xml,
+ spin: 'Updating page'
+ }), '*');
+ }
+ }
+ };
+
+ window.addEventListener('message', receive);
+ iframe.setAttribute('src', phorge_extension.editor);
+ document.querySelector("#mainScreen").appendChild(iframe);
+ iframe.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) {
+ 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.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.diagramName = diagramName;
+ phorge_extension.diagramVersion = diagramVersion;
+ phorge_extension.diagramBase64 = diagramBase64;
+
+ document.addEventListener('DOMContentLoaded', function () {
+ edit(document.querySelector('img.drawio'));
+ }, false);
+}
+
+function saveFlowchart(name, flowchartData, iframe) {
+ var diagramID = document.querySelector(".diagramName a").innerText.replace(/^DIAG/, "");
+ var csrf = document.querySelector('input[name="__csrf__"]')?.value;
+ var data = new URLSearchParams();
+ data.append('data', flowchartData);
+ data.append('diagramID', diagramID);
+ data.append('__csrf__', csrf);
+ data.append('__form__', '1');
+
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.overrideMimeType("application/json");
+ xmlhttp.open('POST', "save/", true);
+ xmlhttp.onload = function () {
+ if (xmlhttp.readyState == 4) {
+ var errorMessage = null;
+ try {
+ var result = JSON.parse(xmlhttp.responseText);
+ if (result.Status != "OK") {
+ errorMessage = result.Error;
+ } else {
+ // make sure we don't show messagebox about redirection in browser
+ iframe.parentNode.removeChild(iframe);
+
+ if (!phorge_extension.diagramVersion || !phorge_extension.diagramVersion.trim()) {
+ if (!phorge_extension.diagramName || !phorge_extension.diagramName.trim()) {
+ // load new diagram
+ window.location = window.location + "/DIAG" + result.DiagramID;
+ } else {
+ // reload actual page (so versioned diagrams info is also updated)
+ window.location.reload();
+ }
+ } else {
+ // cut off version id from url
+ var url = document.baseURI
+ .substring(0,
+ document.baseURI
+ .length
+ - phorge_extension.diagramVersion
+ .length
+ - 1
+ )
+ // redirect to latest version of diagram
+ window.location = url;
+ }
+ }
+ } catch (exc) {
+ errorMessage = exc.message;
+ }
+ }
+ };
+
+ xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;');
+ xmlhttp.send(data);
+}
+
+function setupButtonsInMenuToolbar() {
+ var iframe = document.querySelector('iframe');
+ var btnSave = Array.prototype.slice.call(
+ iframe.contentDocument
+ .querySelector('.geMenubarContainer')
+ .querySelectorAll('button'), 0
+ ).reverse()[0];
+
+ // identify Exit Button
+ btnSave.classList.add('btnSave');
+
+ // 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';
+ dropdown.appendChild(toggle);
+
+ // create the dropdown menu
+ const menu = document.createElement('div');
+ menu.classList.add('dropdown-menu');
+ dropdown.appendChild(menu);
+
+ // 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);
+
+ // add the corresponding CSS
+ const style = document.createElement('style');
+ style.textContent = `
+ .dropdown {
+ position: fixed;
+ right: 0;
+ white-space: nowrap;
+ }
+
+ .hasversions .btnSave {
+ margin-right: 104px;
+ }
+
+ .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%;
+ 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 {
+ display: block;
+ z-index: 3;
+ }
+
+ .dropdown .dropdown-menu .menu-item {
+ padding: 2px;
+ }
+
+ .dropdown .dropdown-menu .menu-item a {
+ line-height: 20px;
+ }
+
+ .dropdown .dropdown-menu .menu-item:hover {
+ background: #29b6f2;
+ color: #fff;
+ margin-left: -2px;
+ margin-right: 2px;
+ }
+
+ .dropdown .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');
+ });
+
+ prevBtn.addEventListener('click', (event) => {
+ event.stopPropagation();
+
+ var versionList = document.querySelector('.version-list');
+ var pagecount = parseInt(versionList.dataset.pagecount);
+
+ if (currentPage > 1) {
+ currentPage--;
+ requestVersionInfo(currentPage);
+ }
+
+ prevBtn.disabled = (currentPage <= 1);
+ nextBtn.disabled = (currentPage >= pagecount);
+ });
+
+ nextBtn.addEventListener('click', (event) => {
+ event.stopPropagation();
+
+ var versionList = document.querySelector('.version-list');
+ var pagecount = parseInt(versionList.dataset.pagecount);
+
+ if (currentPage <= pagecount) {
+ currentPage++;
+ requestVersionInfo(currentPage);
+ }
+
+ prevBtn.disabled = (currentPage <= 1);
+ nextBtn.disabled = (currentPage >= pagecount);
+ });
+
+ document.addEventListener('click', (event) => {
+ if (!dropdown.contains(event.target)) {
+ dropdown.classList.remove('open');
+ }
+ });
+ `;
+ iframe.contentDocument.body.appendChild(script);
+}
\ No newline at end of file
diff --git a/resources/sql/20230626.DiagramCreateTables.sql b/resources/sql/20230626.DiagramCreateTables.sql
new file mode 100644
index 0000000..a0653e5
--- /dev/null
+++ b/resources/sql/20230626.DiagramCreateTables.sql
@@ -0,0 +1,23 @@
+CREATE TABLE IF NOT EXISTS {$NAMESPACE}_diagram.diagram (
+ id int(10) unsigned NOT NULL AUTO_INCREMENT,
+
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS {$NAMESPACE}_diagram.diagram_version (
+ id int(10) unsigned NOT NULL AUTO_INCREMENT,
+ diagramID int(10) unsigned NOT NULL,
+ version int(10) unsigned NOT NULL,
+ phid varbinary(64) NOT NULL,
+ authorPHID varbinary(64) NOT NULL,
+ dateCreated int(10) unsigned NOT NULL,
+ dateModified int(10) unsigned NOT NULL,
+ byteSize bigint(20) unsigned NOT NULL,
+ data longblob NOT NULL,
+ viewPolicy varbinary(64) NOT NULL,
+ editPolicy varbinary(64) NOT NULL,
+
+ PRIMARY KEY (id),
+ UNIQUE KEY key_diagramID_version (diagramID, version),
+ KEY key_authorPHID (authorPHID)
+);
\ No newline at end of file
diff --git a/src/.phutil_module_cache b/src/.phutil_module_cache
new file mode 100644
index 0000000..bc9d6e2
--- /dev/null
+++ b/src/.phutil_module_cache
@@ -0,0 +1 @@
+{"__symbol_cache_version__":11,"3e1aef3fd93a3c689644ae239df4b2e5":{"have":{"class":{"DiagramApplication":18}},"need":{"function":{"pht":108},"class":{"PhabricatorApplication":45}},"xmap":{"DiagramApplication":["PhabricatorApplication"]}},"d099bb1c91140ff4a0dc595017eeae10":{"have":{"class":{"UnsafeWebpageResponse":19}},"need":{"function":{"phutil_nonempty_string":567,"phutil_tag":935,"hsprintf":1122},"class":{"AphrontHTMLResponse":49}},"xmap":{"UnsafeWebpageResponse":["AphrontHTMLResponse"]}},"b9da8fb011ef341de7b9362b362f4ad4":{"have":{"class":{"PhabricatorRemarkupDiagramRule":19}},"need":{"function":{"id":278,"phutil_escape_html":1580,"phutil_tag":1654},"class":{"PhabricatorObjectRemarkupRule":58,"PhabricatorDiagramQuery":285},"class\/interface":{"PhabricatorObjectHandle":532}},"xmap":{"PhabricatorRemarkupDiagramRule":["PhabricatorObjectRemarkupRule"]}},"726b951bddd81b2e31ac733a1ac6e047":{"have":[],"need":[],"xmap":[]},"b37e401a0f7d897abef8f31ac9f562e8":{"have":{"class":{"DiagramPatchList":19}},"need":{"function":{"phutil_get_library_root":186},"class":{"PhabricatorSQLPatchList":44}},"xmap":{"DiagramPatchList":["PhabricatorSQLPatchList"]}},"70623bab70378fedfb0911949f0ac425":{"have":{"class":{"DiagramDAO":22}},"need":{"class":{"PhabricatorLiskDAO":41}},"xmap":{"DiagramDAO":["PhabricatorLiskDAO"]}},"73009923d74464970c779f30e0ff630b":{"have":{"class":{"PhabricatorDiagramQuery":19}},"need":{"function":{"queryfx_all":365,"qsprintf":1103},"class":{"PhabricatorCursorPagedPolicyAwareQuery":51,"DiagramVersion":286,"DiagramApplication":1314},"class\/interface":{"AphrontDatabaseConnection":988}},"xmap":{"PhabricatorDiagramQuery":["PhabricatorCursorPagedPolicyAwareQuery"]}},"79775b0350f1b39bb4bad0feb922b459":{"have":{"class":{"Diagram":19}},"need":{"function":{"queryfx":3447},"class":{"DiagramDAO":35,"PhabricatorPolicyCapability":921,"PhabricatorPolicies":2379},"class\/interface":{"PhabricatorDestructionEngine":525,"PhabricatorUser":2775},"interface":{"PhabricatorPolicyInterface":59,"PhabricatorSubscribableInterface":89,"PhabricatorDestructibleInterface":125}},"xmap":{"Diagram":["DiagramDAO","PhabricatorPolicyInterface","PhabricatorSubscribableInterface","PhabricatorDestructibleInterface"]}},"d16b913b4e9d67f4ca6e121837f46414":{"have":{"class":{"DiagramVersion":19}},"need":{"function":{"pht":1432,"id":3327,"idx":3686,"queryfx_one":4900,"queryfx":8015},"class":{"DiagramDAO":42,"PhabricatorDiagramQuery":5841,"Diagram":6186,"PhabricatorPolicyCapability":744,"PhabricatorPolicies":1218,"PhabricatorUser":5888},"class\/interface":{"PhabricatorUser":3192,"PhabricatorDestructionEngine":4451},"interface":{"PhabricatorDestructibleInterface":66,"PhabricatorPolicyInterface":102}},"xmap":{"DiagramVersion":["DiagramDAO","PhabricatorDestructibleInterface","PhabricatorPolicyInterface"]}},"a7879a2f6d74e461baa1c8e2e05d003c":{"have":{"class":{"DiagramController":18}},"need":{"function":{"id":680,"phutil_get_library_root":1037,"phutil_tag":5335,"phutil_safe_html":7820},"class":{"PhabricatorController":44,"DiagramVersion":687,"AphrontFileResponse":794,"UnsafeWebpageResponse":2396,"AphrontJSONResponse":4726,"PhabricatorStandardPageView":9305,"AphrontWebpageResponse":9507,"PhabricatorPolicies":4420},"class\/interface":{"AphrontRequest":100}},"xmap":{"DiagramController":["PhabricatorController"]}}}
\ No newline at end of file
diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php
new file mode 100644
index 0000000..cfd40af
--- /dev/null
+++ b/src/application/DiagramApplication.php
@@ -0,0 +1,76 @@
+<?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",
+
+ // other urls
+ ".*"
+ => "DiagramController",
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php
new file mode 100644
index 0000000..ac466e8
--- /dev/null
+++ b/src/controller/DiagramController.php
@@ -0,0 +1,509 @@
+<?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);
+ } else {
+ $diagram = id(new DiagramVersion())->loadByVersionedDiagramID($diagram_id, $version);
+ }
+ if ($diagram) {
+ $data = $diagram->getData();
+ $base64_data = base64_encode($data);
+
+ return $this->showApplication(
+ $request,
+ 'DIAG' . $diagram_id,
+ $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.
+ */
+ private 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)) {
+ $chunk = unpack('Nlength/a4type', fread($fp, 8));
+ 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) {
+ $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_base64_data = base64_encode($data);
+
+ if ($this->equalPngMetaData($base64_data, $old_base64_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);
+
+ $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;
+ }
+ }
+
+ /**
+ * Shows the draw.io application integrated in Phorge's layout
+ */
+ private function showApplication(
+ AphrontRequest $request,
+ string $diagramName = null,
+ string $diagramVersion = null,
+ string $diagramBase64 = null
+ ) {
+ $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 . '
+ ),
+ 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
+ . '", "'
+ . $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' => '.'
+ ),
+ array(
+ phutil_tag(
+ 'span',
+ array(
+ 'class' => 'phui-font-fa fa-sitemap',
+ 'style' => 'padding-right:5px;'
+ ))
+ )),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => '.'
+ ),
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/src/query/PhabricatorDiagramQuery.php b/src/query/PhabricatorDiagramQuery.php
new file mode 100644
index 0000000..8302219
--- /dev/null
+++ b/src/query/PhabricatorDiagramQuery.php
@@ -0,0 +1,73 @@
+<?php
+
+final class PhabricatorDiagramQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ protected $diagramIDs;
+
+ public function withDiagramIDs(array $diagram_ids) {
+ $this->diagramIDs = $diagram_ids;
+ 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 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 %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();
+
+ if ($this->diagramIDs !== null) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'diagramID IN (%Ld)',
+ $this->diagramIDs);
+ }
+
+ return $this->formatWhereClause($conn_r, $where);
+ }
+
+ public function getQueryApplicationClass() {
+ return DiagramApplication::class;
+ }
+
+}
+
diff --git a/src/remarkup/PhabricatorRemarkupDiagramRule.php b/src/remarkup/PhabricatorRemarkupDiagramRule.php
new file mode 100644
index 0000000..45c8c90
--- /dev/null
+++ b/src/remarkup/PhabricatorRemarkupDiagramRule.php
@@ -0,0 +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())
+ ->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/response/PlainHtmlWebpageResponse.php b/src/response/PlainHtmlWebpageResponse.php
new file mode 100644
index 0000000..4272260
--- /dev/null
+++ b/src/response/PlainHtmlWebpageResponse.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * AphrontHTMLResponse class which allows you to set its content to
+ * plain HTML without having Phorge trying to re-encode it to HTML.
+ *
+ * The HTML content from the IFrame can then be forwarded to Phorge
+ * without any issues
+ */
+final class PlainHtmlWebpageResponse extends AphrontHTMLResponse {
+
+ private $content;
+ private $unexpectedOutput;
+
+ public function setContent($content) {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function setUnexpectedOutput($unexpected_output) {
+ $this->unexpectedOutput = $unexpected_output;
+ return $this;
+ }
+
+ public function getUnexpectedOutput() {
+ return $this->unexpectedOutput;
+ }
+
+ public function buildResponseString() {
+ $unexpected_output = $this->getUnexpectedOutput();
+ if (phutil_nonempty_string($unexpected_output)) {
+ // in case we get some unexpected output (e.g. a stacktraced error)
+ // the output is shown on top of the screen in a red banner
+ $style = array(
+ 'background: linear-gradient(180deg, #eeddff, #ddbbff);',
+ 'white-space: pre-wrap;',
+ 'z-index: 200000;',
+ 'position: relative;',
+ 'padding: 16px;',
+ 'font-family: monospace;',
+ 'text-shadow: 1px 1px 1px white;'
+ );
+
+ $unexpected_header = phutil_tag(
+ 'div',
+ array(
+ 'style' => implode(' ', $style),
+ ),
+ $unexpected_output);
+ } else {
+ $unexpected_header = '';
+ }
+
+ // print output as is
+ return hsprintf('%s', $unexpected_header) . $this->content;
+ }
+
+}
diff --git a/src/storage/Diagram.php b/src/storage/Diagram.php
new file mode 100644
index 0000000..0432782
--- /dev/null
+++ b/src/storage/Diagram.php
@@ -0,0 +1,142 @@
+<?php
+
+final class Diagram extends DiagramDAO implements
+ PhabricatorPolicyInterface,
+ PhabricatorSubscribableInterface,
+ PhabricatorDestructibleInterface
+{
+
+ /**
+ * List of properties mapped to database table columns
+ */
+ protected $id;
+
+ /**
+ * 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,
+ );
+ }
+
+ /**
+ * Configures application-wide storage settings.
+ * This creates a mapping of the corresponding database table.
+ */
+ public function getConfiguration()
+ {
+ return array(
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'id' => 'auto',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'PRIMARY' => array(
+ 'columns' => array('id'),
+ 'unique' => true,
+ ),
+ ),
+ );
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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 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;
+ }
+
+ /**
+ * 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)
+ );
+
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %T () VALUES ()',
+ $table_name
+ );
+
+ $this->id = $conn_w->getInsertID();
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/src/storage/DiagramDAO.php b/src/storage/DiagramDAO.php
new file mode 100644
index 0000000..cc26e3a
--- /dev/null
+++ b/src/storage/DiagramDAO.php
@@ -0,0 +1,14 @@
+<?php
+
+abstract class DiagramDAO extends PhabricatorLiskDAO {
+
+ /**
+ * Return the name of the application that this object type belongs to.
+ * For example, if you are creating a custom application, you would
+ * return the name of your custom application here.
+ */
+ public function getApplicationName() {
+ return 'diagram';
+ }
+
+}
\ No newline at end of file
diff --git a/src/storage/DiagramVersion.php b/src/storage/DiagramVersion.php
new file mode 100644
index 0000000..be98448
--- /dev/null
+++ b/src/storage/DiagramVersion.php
@@ -0,0 +1,342 @@
+<?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;
+
+ /**
+ * 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;
+ }
+
+ public static function initializeNewDiagram(PhabricatorUser $actor) {
+ return id(new DiagramVersion())
+ ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
+ ->setEditPolicy($actor->getPHID())
+ ->setAuthorPHID($actor->getPHID())
+ ->setVersion(1)
+ ->setDateCreated(time())
+ ->setDateModified(time());
+ }
+
+ 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;
+ }
+
+ public function attachProjectPHIDs(array $phids) {
+ // Attach an array of project PHIDs to this object. This is used by the
+ // project system to manage project membership and visibility for this
+ // object.
+ }
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine
+ ) {
+ // Permanently destroy this object. This is used by the destructible
+ // interface to allow administrators to permanently delete objects from
+ // the system.
+ $this->openTransaction();
+ $this->delete();
+ $this->saveTransaction();
+ }
+
+ 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;
+ }
+
+ 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
+ );
+ }
+
+ 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
+ );
+ }
+
+ public function loadByDiagramPHID($diagramPHID) {
+ if (is_object($diagramPHID)) {
+ $diagramPHID = (string) $diagramPHID;
+ }
+
+ return $this->loadOneWhere(
+ 'phid = %s ORDER BY version DESC LIMIT 1',
+ $diagramPHID
+ );
+ }
+
+ 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
+ );
+ }
+
+ public function save() {
+ // Load the last record with the same PHID.
+ $last_record = null;
+ if ($this->getDiagramID() !== null) {
+ $last_record = id(new PhabricatorDiagramQuery())
+ ->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())
+ ->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;
+ }
+}
\ No newline at end of file
diff --git a/src/storage/patch/DiagramPatchList.php b/src/storage/patch/DiagramPatchList.php
new file mode 100644
index 0000000..8e14cde
--- /dev/null
+++ b/src/storage/patch/DiagramPatchList.php
@@ -0,0 +1,27 @@
+<?php
+
+final class DiagramPatchList extends PhabricatorSQLPatchList {
+
+ public function getNamespace() {
+ return 'diagram';
+ }
+
+ public function getPatches() {
+ $root = dirname(phutil_get_library_root('diagram'));
+
+ $db_patches = array(
+ // create database
+ 'db.diagram' => array(
+ 'name' => 'diagram',
+ 'type' => 'db',
+ 'after' => array(),
+ ),
+ );
+
+
+ $auto = $this->buildPatchesFromDirectory($root.'/resources/sql/');
+ $result = $db_patches + $auto;
+ return $result;
+ }
+
+}
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 21:44 (6 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129070
Default Alt Text
(71 KB)
Attached To
Mode
R5 Diagrams
Attached
Detach File
Event Timeline
Log In to Comment