Page MenuHomePhorge

No OneTemporary

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

Mime Type
text/x-diff
Expires
Jan 19 2025, 21:44 (6 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129070
Default Alt Text
(71 KB)

Event Timeline