├── Diagrams.php ├── LICENSE ├── README ├── action ├── action.php ├── embed.php ├── mediafile.php └── prosemirror.php ├── all.less ├── conf ├── default.php └── metadata.php ├── deleted.files ├── helper.php ├── img ├── diagramsnet.png └── diagramsnet.svg ├── lang ├── de │ ├── lang.php │ └── settings.php ├── en │ ├── lang.php │ └── settings.php ├── fr │ ├── lang.php │ └── settings.php ├── ko │ ├── lang.php │ └── settings.php ├── ru │ ├── lang.php │ └── settings.php └── vi │ ├── lang.php │ └── settings.php ├── parser └── DiagramsNode.php ├── plugin.info.txt ├── renderer.php ├── script.js ├── script ├── ButtonFunctions.js ├── DiagramsEditor.js ├── DiagramsForm.js ├── DiagramsMediaManager.js ├── DiagramsMenuItemDispatcher.js ├── DiagramsView.js ├── download.js ├── embed-editbutton.js ├── embed-toolbar.js ├── mediafile-editbutton.js ├── mediafile-linkfix.js └── prosemirror.js ├── svg.svg └── syntax ├── embed.php └── mediafile.php /Diagrams.php: -------------------------------------------------------------------------------- 1 | "'none'", 15 | 'style-src' => "'unsafe-inline' fonts.googleapis.com", 16 | 'media-src' => "'self'", 17 | 'object-src' => "'self'", 18 | 'font-src' => "'self' data: fonts.gstatic.com", 19 | 'form-action' => "'none'", 20 | 'frame-ancestors' => "'self'", 21 | 'img-src' => "self data:", 22 | 'sandbox' => "allow-popups allow-popups-to-escape-sandbox allow-top-navigation allow-same-origin", 23 | ]; 24 | 25 | const CACHE_EXT = '.diagrams.png'; 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | 342 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | diagrams Plugin for DokuWiki 2 | 3 | Integration of diagrams.net. Create and edit diagrams in DokuWiki. 4 | 5 | All documentation for this plugin can be found at 6 | https://www.dokuwiki.org/plugin:diagrams 7 | 8 | If you install this plugin manually, make sure it is installed in 9 | lib/plugins/diagrams/ - if the folder is called different it 10 | will not work! 11 | 12 | Please refer to http://www.dokuwiki.org/plugins for additional info 13 | on how to install plugins in DokuWiki. 14 | 15 | ---- 16 | Copyright (C) CosmoCode GmbH 17 | 18 | This program is free software; you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License as published by 20 | the Free Software Foundation; version 2 of the License 21 | 22 | This program is distributed in the hope that it will be useful, 23 | but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | GNU General Public License for more details. 26 | 27 | See the COPYING file in your DokuWiki folder for details 28 | -------------------------------------------------------------------------------- /action/action.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class action_plugin_diagrams_action extends DokuWiki_Action_Plugin 12 | { 13 | /** @var helper_plugin_diagrams */ 14 | protected $helper; 15 | 16 | /**@inheritDoc */ 17 | public function register(Doku_Event_Handler $controller) 18 | { 19 | $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'addJsinfo'); 20 | $controller->register_hook('MEDIAMANAGER_STARTED', 'AFTER', $this, 'addJsinfo'); 21 | $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'checkConf'); 22 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleCache'); 23 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handlePNGDownload'); 24 | 25 | $this->helper = plugin_load('helper', 'diagrams'); 26 | } 27 | 28 | /** 29 | * Add data to JSINFO 30 | * 31 | * full service URL 32 | * digram mode 33 | * security token used for uploading 34 | * 35 | * @param Doku_Event $event DOKUWIKI_STARTED|MEDIAMANAGER_STARTED 36 | */ 37 | public function addJsinfo(Doku_Event $event) 38 | { 39 | global $JSINFO; 40 | $JSINFO['sectok'] = getSecurityToken(); 41 | $JSINFO['plugins']['diagrams'] = [ 42 | 'service_url' => $this->getConf('service_url'), 43 | 'mode' => $this->getConf('mode'), 44 | ]; 45 | } 46 | 47 | /** 48 | * Check if DokuWiki is properly configured to handle SVG diagrams 49 | * 50 | * @param Doku_Event $event DOKUWIKI_STARTED 51 | */ 52 | public function checkConf(Doku_Event $event) 53 | { 54 | $mime = getMimeTypes(); 55 | if (!array_key_exists('svg', $mime) || $mime['svg'] !== 'image/svg+xml') { 56 | msg($this->getLang('missingConfig'), -1); 57 | } 58 | } 59 | 60 | /** 61 | * Save the PNG cache of a diagram 62 | * 63 | * @param Doku_Event $event AJAX_CALL_UNKNOWN 64 | */ 65 | public function handleCache(Doku_Event $event) 66 | { 67 | if ($event->data !== 'plugin_diagrams_savecache') return; 68 | $event->preventDefault(); 69 | $event->stopPropagation(); 70 | 71 | // to not further complicate the JavaScript and because creating the PNG is essentially free, 72 | // we always create the PNG but only save it if the cache is enabled 73 | if (!$this->getConf('pngcache')) { 74 | echo 'PNG cache disabled, call ignored'; 75 | return; 76 | } 77 | 78 | global $INPUT; 79 | 80 | $svg = $INPUT->str('svg'); // raw svg 81 | $png = $INPUT->str('png'); // data uri 82 | 83 | if (!checkSecurityToken()) { 84 | http_status(403); 85 | return; 86 | } 87 | 88 | if (!$this->helper->isDiagram($svg)) { 89 | http_status(400); 90 | return; 91 | } 92 | 93 | if (!preg_match('/^data:image\/png;base64,/', $png)) { 94 | http_status(400); 95 | return; 96 | } 97 | $png = base64_decode(explode(',', $png)[1]); 98 | 99 | if (substr($png, 1, 3) !== 'PNG') { 100 | http_status(400); 101 | return; 102 | } 103 | 104 | $cacheName = getCacheName($svg, '.diagrams.png'); 105 | if (io_saveFile($cacheName, $png)) { 106 | echo 'OK'; 107 | } else { 108 | http_status(500); 109 | } 110 | } 111 | 112 | /** 113 | * PNG download available via link created in JS (only if PNG caching is enabled) 114 | * 115 | * @param Doku_Event $event 116 | * @return void 117 | */ 118 | public function handlePNGDownload(Doku_Event $event) 119 | { 120 | if ($event->data !== 'plugin_diagrams_pngdownload') return; 121 | $event->preventDefault(); 122 | $event->stopPropagation(); 123 | 124 | global $INPUT; 125 | global $conf; 126 | 127 | $cacheName = $INPUT->str('pngcache'); 128 | $media = cleanID($INPUT->str('media')); 129 | $id = cleanID($INPUT->str('id')); 130 | 131 | // check ACLs to original file or page 132 | if ( 133 | ($id && auth_quickaclcheck($id) < AUTH_READ) || 134 | ($media && auth_quickaclcheck($media) < AUTH_READ) 135 | ) { 136 | http_status(403); 137 | return; 138 | } 139 | 140 | // check if download target exists 141 | if ( 142 | ($id && !page_exists($id)) || 143 | ($media && !media_exists($media)) 144 | ) { 145 | http_status(404); 146 | return; 147 | } 148 | 149 | // serve cached PNG file 150 | $file = $conf['cachedir'] . $cacheName . \dokuwiki\plugin\diagrams\Diagrams::CACHE_EXT; 151 | if (file_exists($file)) { 152 | // correct file extension 153 | $download = $media ? str_replace('.svg', '.png', $media) : $id . ".png"; 154 | $download = noNS($download); 155 | header('Content-Type: image/png'); 156 | header("Content-Disposition: attachment; filename=$download;"); 157 | http_sendfile($file); 158 | readfile($file); 159 | } else { 160 | http_status(404); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /action/embed.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class action_plugin_diagrams_embed extends \dokuwiki\Extension\ActionPlugin 14 | { 15 | /** @var helper_plugin_diagrams */ 16 | protected $helper; 17 | 18 | /** @inheritDoc */ 19 | public function register(Doku_Event_Handler $controller) 20 | { 21 | // only register if embed mode is enabled 22 | if (!($this->getConf('mode') & Diagrams::MODE_EMBED)) return; 23 | 24 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleLoad'); 25 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleSave'); 26 | 27 | $this->helper = plugin_load('helper', 'diagrams'); 28 | } 29 | 30 | /** 31 | * Load the SVG for an embedded diagram 32 | * 33 | * This locks the page for editing 34 | * 35 | * @param Doku_Event $event Event object AJAX_CALL_UNKNOWN 36 | */ 37 | public function handleLoad(Doku_Event $event) 38 | { 39 | if ($event->data !== 'plugin_diagrams_embed_load') return; 40 | $event->preventDefault(); 41 | $event->stopPropagation(); 42 | 43 | global $INPUT; 44 | 45 | $id = $INPUT->str('id'); 46 | $pos = $INPUT->int('pos'); 47 | $len = $INPUT->int('len'); 48 | 49 | if (auth_quickaclcheck($id) < AUTH_EDIT) { 50 | http_status(403); 51 | return; 52 | } 53 | 54 | if (!page_exists($id)) { 55 | http_status(404); 56 | return; 57 | } 58 | 59 | if (checklock($id)) { 60 | http_status(423, 'Page Locked'); 61 | return; 62 | } 63 | 64 | $svg = rawWiki($id); 65 | $svg = substr($svg, $pos, $len); 66 | if (!$this->helper->isDiagram($svg)) { 67 | http_status(400); 68 | return; 69 | } 70 | 71 | lock($id); // FIXME we probably need some periodic lock renewal while editing? 72 | header('Content-Type: image/svg+xml'); 73 | echo $svg; 74 | } 75 | 76 | /** 77 | * Save a new embedded diagram 78 | * 79 | * @param Doku_Event $event AJAX_CALL_UNKNOWN 80 | */ 81 | public function handleSave(Doku_Event $event) 82 | { 83 | if ($event->data !== 'plugin_diagrams_embed_save') return; 84 | $event->preventDefault(); 85 | $event->stopPropagation(); 86 | 87 | global $INPUT; 88 | 89 | $id = $INPUT->str('id'); 90 | $svg = $INPUT->str('svg'); 91 | $pos = $INPUT->int('pos'); 92 | $len = $INPUT->int('len'); 93 | 94 | 95 | if (auth_quickaclcheck($id) < AUTH_EDIT) { 96 | http_status(403); 97 | return; 98 | } 99 | 100 | if (!page_exists($id)) { 101 | http_status(404); 102 | return; 103 | } 104 | 105 | if (!checkSecurityToken()) { 106 | http_status(403); 107 | return; 108 | } 109 | 110 | if (!$this->helper->isDiagram($svg)) { 111 | http_status(400); 112 | return; 113 | } 114 | 115 | $original = rawWiki($id); 116 | $new = substr($original, 0, $pos) . $svg . substr($original, $pos + $len); 117 | saveWikiText($id, $new, $this->getLang('embedSaveSummary')); 118 | unlock($id); 119 | echo 'OK'; 120 | } 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /action/mediafile.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class action_plugin_diagrams_mediafile extends DokuWiki_Action_Plugin 14 | { 15 | 16 | /** @var helper_plugin_diagrams */ 17 | protected $helper; 18 | 19 | /** @inheritDoc */ 20 | public function register(Doku_Event_Handler $controller) 21 | { 22 | // only register if mediafile mode is enabled 23 | if (!($this->getConf('mode') & Diagrams::MODE_MEDIA)) return; 24 | 25 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleEditCheck'); 26 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleNamespaceCheck'); 27 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleExistsCheck'); 28 | $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleIsDiagramCheck'); 29 | $controller->register_hook('MEDIA_SENDFILE', 'BEFORE', $this, 'handleCSP'); 30 | $controller->register_hook('PLUGIN_MOVE_HANDLERS_REGISTER', 'BEFORE', $this, 'registerMoveHandler'); 31 | 32 | $this->helper = plugin_load('helper', 'diagrams'); 33 | } 34 | 35 | public function handleExistsCheck(Doku_Event $event) 36 | { 37 | if ($event->data !== 'plugin_diagrams_mediafile_existscheck') return; 38 | $event->preventDefault(); 39 | $event->stopPropagation(); 40 | 41 | global $INPUT; 42 | $mediaId = $INPUT->str('mediaId'); 43 | 44 | $file = mediaFN($mediaId); 45 | 46 | echo json_encode(file_exists($file)); 47 | } 48 | 49 | /** 50 | * Check all supplied diagrams and return only editable diagrams 51 | * 52 | * @param Doku_Event $event AJAX_CALL_UNKNOWN 53 | */ 54 | public function handleEditCheck(Doku_Event $event) 55 | { 56 | if ($event->data !== 'plugin_diagrams_mediafile_editcheck') return; 57 | $event->preventDefault(); 58 | $event->stopPropagation(); 59 | 60 | global $INPUT; 61 | $diagrams = (array)json_decode($INPUT->str('diagrams')); 62 | 63 | $editable = []; 64 | foreach ($diagrams as $image) { 65 | $image = cleanID($image); 66 | $file = mediaFN($image); 67 | 68 | if ( 69 | file_exists($file) && 70 | auth_quickaclcheck($image) >= AUTH_UPLOAD && 71 | $this->helper->isDiagramFile($file) 72 | ) { 73 | $editable[] = $image; 74 | } 75 | } 76 | 77 | echo json_encode($editable); 78 | } 79 | 80 | /** 81 | * Check if the given media ID is a diagram 82 | * 83 | * @param Doku_Event $event AJAX_CALL_UNKNOWN 84 | */ 85 | public function handleIsDiagramCheck(Doku_Event $event) 86 | { 87 | if ($event->data !== 'plugin_diagrams_mediafile_isdiagramcheck') return; 88 | $event->preventDefault(); 89 | $event->stopPropagation(); 90 | 91 | global $INPUT; 92 | $diagram = $INPUT->str('diagram'); 93 | 94 | $file = mediaFN(cleanID($diagram)); 95 | if (!file_exists($file)) { 96 | http_status(404); 97 | echo 0; 98 | return; 99 | } 100 | 101 | if (!$this->helper->isDiagramFile($file)) { 102 | http_status(403); 103 | echo 0; 104 | } 105 | 106 | echo 1; 107 | } 108 | 109 | /** 110 | * Check ACL for supplied namespace 111 | * 112 | * @param Doku_Event $event AJAX_CALL_UNKNOWN 113 | */ 114 | public function handleNamespaceCheck(Doku_Event $event) 115 | { 116 | if ($event->data !== 'plugin_diagrams_mediafile_nscheck') return; 117 | $event->preventDefault(); 118 | $event->stopPropagation(); 119 | 120 | global $INPUT; 121 | $ns = $INPUT->str('ns'); 122 | 123 | echo json_encode(auth_quickaclcheck($ns . ':*') >= AUTH_UPLOAD); 124 | } 125 | 126 | /** 127 | * Set custom CSP for SVG diagrams 128 | * 129 | * @param Doku_Event $event MEDIA_SENDFILE 130 | */ 131 | public function handleCSP(Doku_Event $event) 132 | { 133 | if ($event->data['ext'] === 'svg' && $this->helper->isDiagramFile($event->data['file'])) { 134 | $event->data['csp'] = Diagrams::CSP; 135 | } 136 | } 137 | 138 | /** 139 | * Registers our handler with the move plugin 140 | * 141 | * @param Doku_Event $event 142 | * @return void 143 | */ 144 | public function registerMoveHandler(Doku_Event $event) 145 | { 146 | $event->data['handlers']['diagrams_mediafile'] = [new \syntax_plugin_diagrams_mediafile(), 'handleMove']; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /action/prosemirror.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class action_plugin_diagrams_prosemirror extends \dokuwiki\Extension\ActionPlugin 14 | { 15 | 16 | /** @inheritDoc */ 17 | public function register(Doku_Event_Handler $controller) 18 | { 19 | $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRender'); 20 | $controller->register_hook('PROSEMIRROR_PARSE_UNKNOWN', 'BEFORE', $this, 'handleNode'); 21 | } 22 | 23 | /** 24 | * Event handler for PROSEMIRROR_RENDER_PLUGIN 25 | * Renders DokuWiki's instructions into JSON as required by schema 26 | * 27 | * @param Doku_Event $event Event object 28 | * @return void 29 | */ 30 | public function handleRender(Doku_Event $event) { 31 | /* 32 | $eventData = [ 33 | 'name' => $name, 34 | 'data' => $data, 35 | 'state' => $state, 36 | 'match' => $match, 37 | 'renderer' => $this, 38 | ];*/ 39 | 40 | $eventData = $event->data; 41 | $imageData = $eventData['data']; 42 | 43 | //check for our data 44 | if ( 45 | $eventData['name'] !== 'diagrams_mediafile' && 46 | $eventData['name'] !== 'diagrams_embed' 47 | ) return; 48 | 49 | $event->preventDefault(); 50 | 51 | if($eventData['name'] === 'diagrams_mediafile') { 52 | $url = $imageData['url']; 53 | } else { 54 | // we use a data uri that will be loaded in an img tag 55 | // this should provide the same amount of security as our CSP but can be interactively 56 | // changed during the edit session 57 | $url = 'data:image/svg+xml;base64,' . base64_encode($imageData['svg']); 58 | } 59 | 60 | $node = new Node('diagrams'); 61 | $node->attr('type', $eventData['name'] === 'diagrams_mediafile' ? 'mediafile' : 'embed'); 62 | $node->attr('id', $imageData['src'] ?? ''); // only for mediafile 63 | $node->attr('url', $url); 64 | $node->attr('title', $imageData['title'] ?? ''); 65 | $node->attr('width', $imageData['width']); 66 | $node->attr('height', $imageData['height']); 67 | $node->attr('align', $imageData['align']); 68 | 69 | $event->data['renderer']->addToNodestack($node); 70 | } 71 | 72 | /** 73 | * Event handler for PROSEMIRROR_PARSE_UNKNOWN 74 | * Translate the JSON from Prosemirror back to DokuWiki's syntax 75 | * 76 | * @param Doku_Event $event 77 | * @return void 78 | */ 79 | public function handleNode(Doku_Event $event) 80 | { 81 | /* 82 | $eventData = [ 83 | 'node' => $node, 84 | 'parent' => $parent, 85 | 'previous' => $previous, 86 | 'newNode' => null, 87 | ];*/ 88 | 89 | // check for our node type 90 | if ($event->data['node']['type'] !== 'diagrams') return; 91 | 92 | $event->preventDefault(); 93 | 94 | if($event->data['node']['attrs']['type'] === 'mediafile') { 95 | $node = new ImageNode($event->data['node'], $event->data['parent']); 96 | } else { 97 | $node = new DiagramsNode($event->data['node'], $event->data['parent']); 98 | } 99 | 100 | $event->data['newNode'] = $node; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /all.less: -------------------------------------------------------------------------------- 1 | // link in media manager 2 | a.plugin_diagrams_create { 3 | display: flex; 4 | margin-bottom: 1em; 5 | gap: 0.25em; 6 | 7 | &:before { 8 | content: ''; 9 | width: 1.2em; 10 | height: 1.2em; 11 | background-image: url("img/diagramsnet.svg"); 12 | background-repeat: no-repeat; 13 | } 14 | } 15 | 16 | // in-page display 17 | .diagrams-svg-wrapper { 18 | overflow: auto; 19 | width: fit-content; 20 | height: fit-content; 21 | 22 | // these match the standard dokuwiki template's img alignment 23 | &.media { 24 | margin: .2em 0; 25 | } 26 | 27 | &.medialeft { 28 | margin: .2em 1em .2em 0; 29 | display: inline-block; 30 | } 31 | 32 | &.mediaright { 33 | margin: .2em 0 .2em 1em; 34 | display: inline-block; 35 | } 36 | 37 | &.mediacenter { 38 | margin: .2em auto; 39 | display: block; 40 | } 41 | 42 | object { 43 | // fix for sometimes missing edges 44 | max-width: 99%; 45 | } 46 | } 47 | 48 | // prosemirror node view 49 | .ProseMirror img.diagrams-svg { 50 | cursor: pointer; 51 | } 52 | 53 | // style for the editor itself (on top of everything) 54 | #plugin__diagrams-editor { 55 | border: 0; 56 | position: fixed; 57 | top: 0; 58 | left: 0; 59 | right: 0; 60 | bottom: 0; 61 | width: 100%; 62 | height: 100%; 63 | z-index: 9999; 64 | } 65 | 66 | // buttons 67 | .diagrams-svg-wrapper .diagrams-buttons .diagrams-btn { 68 | @media print { 69 | display: none; 70 | } 71 | 72 | font-size: 0.75em; 73 | 74 | svg { 75 | margin-right: 0.25em; 76 | position: relative; 77 | top: 0.2em; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /conf/default.php: -------------------------------------------------------------------------------- 1 | array(1, 2, 3)); 7 | $meta['pngcache'] = array('onoff'); 8 | -------------------------------------------------------------------------------- /deleted.files: -------------------------------------------------------------------------------- 1 | # This is a list of files that were present in previous releases 2 | # but were removed later. They should not exist in your installation. 3 | MenuItem.php 4 | action.php 5 | diagrams.png 6 | drawing.svg 7 | script/elements.js 8 | script/embed.js 9 | script/helpers.js 10 | script/mediamanager.js 11 | script/service.js 12 | syntax.php 13 | syntax/inline.php 14 | styles.less 15 | -------------------------------------------------------------------------------- /helper.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class helper_plugin_diagrams extends \dokuwiki\Extension\Plugin 10 | { 11 | /** 12 | * Check if the given file is a diagrams.net diagram 13 | * 14 | * @param string $file 15 | * @return bool 16 | */ 17 | public function isDiagramFile($file) 18 | { 19 | $svg = file_get_contents($file, false, null, 0, 500); 20 | return $this->isDiagram($svg); 21 | } 22 | 23 | /** 24 | * Check if the given SVG is a diagrams.net diagram 25 | * 26 | * This is done by ensuring that the service host is part of the SVG header 27 | * 28 | * @param string $svg The raw SVG data (first 500 bytes are enough) 29 | * @return bool 30 | */ 31 | public function isDiagram($svg) 32 | { 33 | $svg = substr($svg, 0, 500); // makes checking a tiny bit faster 34 | $svg = preg_replace('/<\?xml.*?>/', '', $svg); 35 | $svg = preg_replace('//', '', $svg); 36 | $svg = preg_replace('//', '', $svg); 37 | $svg = ltrim($svg); 38 | 39 | if (empty($svg) || substr($svg, 0, 4) !== 'getConf('service_url'); // like "https://diagrams.xyz.org/?embed=1&..." 41 | $serviceHost = parse_url($confServiceUrl, PHP_URL_HOST); // Host-Portion of the Url, e.g. "diagrams.xyz.org" 42 | return strpos($svg, 'embed.diagrams.net') || strpos($svg, 'draw.io') || strpos($svg, $serviceHost); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /img/diagramsnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmocode/dokuwiki-plugin-diagrams/7fcf4f842ec2c44f2fd41145896df5098c007650/img/diagramsnet.png -------------------------------------------------------------------------------- /img/diagramsnet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lang/de/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['missingConfig'] = 'Fehlende Konfiguration für SVG Diagramme: Bitte "svg image/svg+xml" in mime.local.conf ergänzen'; 9 | $lang['embedSaveSummary'] = 'Eingebettetes Diagramm aktualisiert'; 10 | $lang['js']['createButton'] = 'Erstellen'; 11 | $lang['js']['createLink'] = 'Diagramm erstellen'; 12 | $lang['js']['createIntro'] = 'Diagramm im aktuellen Namensraum erstellen:'; 13 | $lang['js']['createForbidden'] = 'Sie besitzen nicht die notwendigen Berechtigungen'; 14 | $lang['js']['editButton'] = 'Diagramm editieren'; 15 | $lang['js']['downloadSVGButton'] = 'Diagramm als SVG herunterladen'; 16 | $lang['js']['downloadPNGButton'] = 'Diagramm als PNG herunterladen'; 17 | $lang['js']['openButton'] = 'Diagramm öffnen'; 18 | $lang['js']['editButtonShort'] = 'Editieren'; 19 | $lang['js']['downloadSVGButtonShort'] = 'SVG Herunterladen'; 20 | $lang['js']['downloadPNGButtonShort'] = 'PNG Herunterladen'; 21 | $lang['js']['openButtonShort'] = 'Öffnen'; 22 | $lang['js']['toolbarButton'] = 'Diagramm erstellen oder bearbeiten'; 23 | $lang['js']['errorInvalidId'] = 'Name ist leer oder enthält ungültige Zeichen!'; 24 | $lang['js']['errorSaving'] = 'Fehler beim Speichern'; 25 | $lang['js']['errorLoading'] = 'Laden fehlgeschlagen'; 26 | $lang['js']['errorUnsupportedFormat'] = 'Nicht unterstütztes Dateiformat!'; 27 | $lang['js']['saving'] = 'Speichern'; 28 | $lang['js']['PMMenuItem-mediafile'] = 'Diagramm verlinken'; 29 | $lang['js']['PMMenuItem-embed'] = 'Diagramm einbetten'; 30 | $lang['js']['selectSource'] = 'Datei wählen'; 31 | $lang['js']['mediaSource'] = 'Mediendatei'; 32 | $lang['js']['alignment'] = 'Ausrichtung'; 33 | $lang['js']['left'] = 'links'; 34 | $lang['js']['center'] = 'zentriert'; 35 | $lang['js']['right'] = 'rechts'; 36 | $lang['js']['title'] = 'Titel'; 37 | $lang['js']['formtitle'] = 'Diagrammeigenschaften'; 38 | $lang['js']['mediafileIsNotDiagram'] = 'Die ausgewählte Mediendatei ist kein Diagramm'; 39 | -------------------------------------------------------------------------------- /lang/de/settings.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Andreas Gohr 8 | */ 9 | $lang['service_url'] = 'URL um den diagrams.net Editor aufzurufen'; 10 | $lang['mode'] = 'Wie sollen Diagramme gespeichert werden?'; 11 | $lang['pngcache'] = 'Sollen Diagramme auch als PNG-Datei zwischengespeichert werden? (empfohlen, wenn das Plugin dw2pdf genutzt wird)'; 12 | $lang['mode_o_1'] = 'Mediendateien'; 13 | $lang['mode_o_2'] = 'In Seite eingebettet'; 14 | $lang['mode_o_3'] = 'Beides erlauben'; 15 | -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | $lang['service_url'] = 'Defines which diagrams.net editor url is used.'; 9 | $lang['mode'] = 'How should diagrams be stored?'; 10 | $lang['pngcache'] = 'Should diagrams also be cached as PNG? (recommended when using the dw2pdf plugin)'; 11 | 12 | $lang['mode_o_1'] = 'Media Files'; 13 | $lang['mode_o_2'] = 'Embedded in the page'; 14 | $lang['mode_o_3'] = 'Allow both'; 15 | -------------------------------------------------------------------------------- /lang/fr/lang.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Schplurtz le Déboulonné 8 | */ 9 | $lang['missingConfig'] = 'Configuration manquante pour les diagrammes SVG : Souvenez vous d\'ajouter "svg image/svg+xml" à conf/mime.local.conf'; 10 | $lang['embedSaveSummary'] = 'Schéma intégré mis à jour'; 11 | $lang['js']['createButton'] = 'Créer'; 12 | $lang['js']['createLink'] = 'Créer un diagramme'; 13 | $lang['js']['createIntro'] = 'Créer un diagramme dans la catégorie courante'; 14 | $lang['js']['createForbidden'] = 'Vos permissions ne sont pas suffisantes'; 15 | $lang['js']['editButton'] = 'Éditer le diagramme'; 16 | $lang['js']['downloadSVGButton'] = 'Télécharger le schéma en SVG'; 17 | $lang['js']['downloadPNGButton'] = 'Télécharger le schéma en PNG'; 18 | $lang['js']['openButton'] = 'Ouvrir le schéma'; 19 | $lang['js']['editButtonShort'] = 'Modifier'; 20 | $lang['js']['downloadSVGButtonShort'] = 'Télécharger en SVG'; 21 | $lang['js']['downloadPNGButtonShort'] = 'Télécharger en PNG'; 22 | $lang['js']['openButtonShort'] = 'Ouvrir'; 23 | $lang['js']['toolbarButton'] = 'Modifier ou créer un diagramme'; 24 | $lang['js']['errorInvalidId'] = 'Le nom est vide ou contient des caractères invalides !'; 25 | $lang['js']['errorSaving'] = 'Échec de l\'enregistrement'; 26 | $lang['js']['errorLoading'] = 'Chargement échoué'; 27 | $lang['js']['errorUnsupportedFormat'] = 'Format de fichier non pris en charge !'; 28 | $lang['js']['saving'] = 'Enregistre…'; 29 | $lang['js']['PMMenuItem-embed'] = 'Intégrer un schéma'; 30 | $lang['js']['selectSource'] = 'Sélectionner un fichier'; 31 | $lang['js']['alignment'] = 'Alignement'; 32 | $lang['js']['left'] = 'gauche'; 33 | $lang['js']['center'] = 'centre'; 34 | $lang['js']['right'] = 'droite'; 35 | $lang['js']['title'] = 'Titre'; 36 | $lang['js']['formtitle'] = 'Propriétés du schéma'; 37 | $lang['js']['mediafileIsNotDiagram'] = 'Le fichier sélectionné n\'est pas un schéma'; 38 | -------------------------------------------------------------------------------- /lang/fr/settings.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Schplurtz le Déboulonné 8 | */ 9 | $lang['service_url'] = 'Définir quel URL d\'éditeur diagrams.net utiliser'; 10 | $lang['mode'] = 'Comment doivent-êtres enregistrés les schémas ?'; 11 | $lang['pngcache'] = 'Faut-il mettre en cache les schémas au format PNG ? (recommandé si utilisation du plugin dw2pdf)'; 12 | $lang['mode_o_1'] = 'Fichiers multimédias'; 13 | $lang['mode_o_2'] = 'Intégré à la page'; 14 | $lang['mode_o_3'] = 'Autoriser les deux'; 15 | -------------------------------------------------------------------------------- /lang/ko/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['js']['createButton'] = '만들기'; 9 | $lang['js']['createLink'] = '다이어그램 만들기'; 10 | $lang['js']['editButton'] = '다이어그램 편집'; 11 | $lang['js']['downloadSVGButton'] = '다이어그램을 SVG로 다운로드'; 12 | $lang['js']['downloadPNGButton'] = '다이어그램을 PNG로 다운로드'; 13 | $lang['js']['openButton'] = '다이어그램 열기'; 14 | $lang['js']['editButtonShort'] = '편집'; 15 | $lang['js']['downloadSVGButtonShort'] = 'SVG 다운로드'; 16 | $lang['js']['downloadPNGButtonShort'] = 'PNG 다운로드'; 17 | $lang['js']['openButtonShort'] = '열기'; 18 | $lang['js']['toolbarButton'] = '다이어그램을 새로 만들거나 편집하기'; 19 | $lang['js']['errorSaving'] = '저장 실패'; 20 | $lang['js']['errorLoading'] = '불러오기 실패'; 21 | $lang['js']['selectSource'] = '파일 선택'; 22 | $lang['js']['left'] = '왼쪽'; 23 | $lang['js']['center'] = '가운데'; 24 | $lang['js']['right'] = '오른쪽'; 25 | $lang['js']['title'] = '제목'; 26 | $lang['js']['mediafileIsNotDiagram'] = '다이어그램이 아닌 미디어 파일이 선택 됨'; 27 | -------------------------------------------------------------------------------- /lang/ko/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['mode_o_1'] = '미디어 파일'; 9 | -------------------------------------------------------------------------------- /lang/ru/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['missingConfig'] = 'Отсутствует настройка для SVG-диаграмм: добавьте строку "svg image/svg+xml" в /conf/mime.local.conf'; 9 | $lang['js']['createButton'] = 'Создать'; 10 | $lang['js']['createLink'] = 'Создать диаграмму'; 11 | $lang['js']['createIntro'] = 'Создать диаграмму в текущем пространстве имён '; 12 | $lang['js']['createForbidden'] = 'У вас недостаточно прав'; 13 | $lang['js']['editButton'] = 'Редактировать диаграмму'; 14 | $lang['js']['downloadSVGButton'] = 'Скачать диаграмму как SVG'; 15 | $lang['js']['downloadPNGButton'] = 'Скачать диаграмму как PNG'; 16 | $lang['js']['openButton'] = 'Открыть диаграмму'; 17 | $lang['js']['editButtonShort'] = 'Править'; 18 | $lang['js']['downloadSVGButtonShort'] = 'Скачать SVG'; 19 | $lang['js']['downloadPNGButtonShort'] = 'Скачать PNG'; 20 | $lang['js']['openButtonShort'] = 'Открыть'; 21 | $lang['js']['toolbarButton'] = 'Редактировать или создать диаграмму'; 22 | $lang['js']['errorInvalidId'] = 'Имя пустое или содержит недопустимые символы!'; 23 | $lang['js']['errorSaving'] = 'Сбой сохранения'; 24 | $lang['js']['errorLoading'] = 'Сбой загрузки'; 25 | $lang['js']['errorUnsupportedFormat'] = 'Неподдерживаемый формат файла!'; 26 | $lang['js']['saving'] = 'Сохранено'; 27 | $lang['js']['selectSource'] = 'Выбрать файл'; 28 | $lang['js']['alignment'] = 'Выравнивание'; 29 | $lang['js']['left'] = 'слева'; 30 | $lang['js']['center'] = 'по центру'; 31 | $lang['js']['right'] = 'справа'; 32 | $lang['js']['title'] = 'Заголовок'; 33 | $lang['js']['formtitle'] = 'Свойства диаграммы'; 34 | $lang['js']['mediafileIsNotDiagram'] = 'Выбранный медиафайл не является диаграммой'; 35 | -------------------------------------------------------------------------------- /lang/ru/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['mode'] = 'Как следует хранить диаграммы?'; 9 | $lang['pngcache'] = 'Кэшировать диаграммы в формате PNG? (рекомендуется при использовании плагина dw2pdf)'; 10 | $lang['mode_o_1'] = 'медиафайлом'; 11 | $lang['mode_o_2'] = 'встраиванием в страницу'; 12 | $lang['mode_o_3'] = 'разрешить оба способа'; 13 | -------------------------------------------------------------------------------- /lang/vi/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['missingConfig'] = 'Thiếu cấu hình cho sơ đồ SVG: Hãy nhớ thêm "hình ảnh svg/svg+xml" vào mime.local.conf'; 9 | $lang['js']['createButton'] = 'Tạo'; 10 | $lang['js']['createLink'] = 'Tạo một sơ đồ'; 11 | $lang['js']['createIntro'] = 'Tạo một sơ đồ trong không gian tên namespace hiện tại'; 12 | $lang['js']['createForbidden'] = 'Bạn không có đủ quyền hạn'; 13 | $lang['js']['editButton'] = 'Chỉnh sửa sơ đồ'; 14 | $lang['js']['errorInvalidId'] = 'Tên bị để trống hoặc có chứa các ký tự không hợp lệ!'; 15 | $lang['js']['errorSaving'] = 'Lưu thất bại'; 16 | $lang['js']['errorUnsupportedFormat'] = 'Định dạng file tệp tin không được hỗ trợ!'; 17 | $lang['js']['saving'] = 'Đang lưu'; 18 | -------------------------------------------------------------------------------- /lang/vi/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['service_url'] = 'Xác định url trình soạn thảo diagrams.net nào được sử dụng.'; 9 | -------------------------------------------------------------------------------- /parser/DiagramsNode.php: -------------------------------------------------------------------------------- 1 | parent = &$parent; 28 | $this->data = $data; 29 | } 30 | 31 | /** @inheritdoc */ 32 | public function toSyntax() 33 | { 34 | $openingTag = 'data['attrs']['align'])) { 36 | $openingTag .= ' ' . $this->data['attrs']['align']; 37 | } 38 | if (!empty($this->data['attrs']['width']) && !empty($this->data['attrs']['height'])) { 39 | $openingTag .= ' ' . $this->data['attrs']['width'] . 'x' . $this->data['attrs']['height']; 40 | } 41 | if (!empty($this->data['attrs']['title'])) { 42 | $openingTag .= ' |' . $this->data['attrs']['title']; 43 | } 44 | $openingTag .= '>'; 45 | 46 | $svg = $this->data['attrs']['url']; 47 | if (substr($svg, 0, 26) !== 'data:image/svg+xml;base64,') { 48 | throw new \Exception('bad data uri "' . substr($svg, 0, 26) . '"'); 49 | } 50 | $svg = base64_decode(substr($svg, 26)); 51 | return $openingTag . $svg . ""; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin.info.txt: -------------------------------------------------------------------------------- 1 | base diagrams 2 | author Innovakom + CosmoCode 3 | email dokuwiki@cosmocode.de 4 | date 2023-12-14 5 | name Diagrams plugin 6 | desc Embed diagrams.net editor (formerly draw.io) 7 | url https://www.dokuwiki.org/plugin:diagrams 8 | -------------------------------------------------------------------------------- /renderer.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class renderer_plugin_diagrams extends Doku_Renderer 12 | { 13 | 14 | /** @inheritDoc */ 15 | public function getFormat() 16 | { 17 | return 'diagrams'; 18 | } 19 | 20 | /** 21 | * Set proper headers 22 | */ 23 | public function document_start() 24 | { 25 | global $ID; 26 | $headers = [ 27 | 'Content-Type' => 'image/svg+xml', 28 | 'Content-Security-Policy' => $this->getCSP(), 29 | ]; 30 | p_set_metadata($ID, ['format' => ['diagrams' => $headers]]); 31 | // don't cache 32 | $this->nocache(); 33 | } 34 | 35 | /** 36 | * Create the content security policy 37 | * @return string 38 | */ 39 | protected function getCSP() { 40 | $policy = Diagrams::CSP; 41 | 42 | /** @noinspection DuplicatedCode from dokuwiki\HTTP\Headers::contentSecurityPolicy() */ 43 | foreach ($policy as $key => $values) { 44 | // if the value is not an array, we also accept newline terminated strings 45 | if (!is_array($values)) $values = explode("\n", $values); 46 | $values = array_map('trim', $values); 47 | $values = array_unique($values); 48 | $values = array_filter($values); 49 | $policy[$key] = $values; 50 | } 51 | 52 | $cspheader = ''; 53 | foreach ($policy as $key => $values) { 54 | if ($values) { 55 | $cspheader .= " $key " . join(' ', $values) . ';'; 56 | } else { 57 | $cspheader .= " $key;"; 58 | } 59 | } 60 | 61 | return $cspheader; 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | /* DOKUWIKI:include script/ButtonFunctions.js */ 2 | /* DOKUWIKI:include script/DiagramsEditor.js */ 3 | /* DOKUWIKI:include script/prosemirror.js */ 4 | 5 | // noinspection JSBitwiseOperatorUsage 6 | if (JSINFO.plugins.diagrams && (JSINFO.plugins.diagrams.mode & 1)) { 7 | /* DOKUWIKI:include script/mediafile-editbutton.js */ 8 | /* DOKUWIKI:include script/DiagramsMediaManager.js */ 9 | } 10 | 11 | // noinspection JSBitwiseOperatorUsage 12 | if (JSINFO.plugins.diagrams && (JSINFO.plugins.diagrams.mode & 2)) { 13 | /* DOKUWIKI:include script/embed-toolbar.js */ 14 | /* DOKUWIKI:include script/embed-editbutton.js */ 15 | } 16 | 17 | /* DOKUWIKI:include script/download.js */ 18 | 19 | // open links in diagrams in the browser window instead of SVG frame 20 | /* DOKUWIKI:include script/mediafile-linkfix.js */ 21 | -------------------------------------------------------------------------------- /script/ButtonFunctions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Button functions 3 | */ 4 | class ButtonFunctions { 5 | 6 | /** 7 | * HTML of a download button 8 | * 9 | * @param {string} ext 10 | * @param {string} identifier 11 | * @param {string} media 12 | * @returns {HTMLAnchorElement} 13 | */ 14 | static getDownloadButton(ext, identifier, media = '') { 15 | 16 | const button = document.createElement('button'); 17 | button.className = 'diagrams-btn'; 18 | 19 | const icon = ButtonFunctions.getButtonIcon('download'); 20 | button.prepend(icon); 21 | 22 | const link = document.createElement('a'); 23 | 24 | if (ext === 'png') { 25 | button.append(LANG.plugins.diagrams.downloadPNGButtonShort); 26 | button.title = LANG.plugins.diagrams.downloadPNGButton; 27 | 28 | let href = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_pngdownload' + 29 | '&pngcache=' + encodeURIComponent(identifier); 30 | 31 | let param; 32 | if (media.length) { 33 | param = '&media=' + encodeURIComponent(media); 34 | } else { 35 | param = '&id=' + JSINFO.id; 36 | } 37 | link.href = href + param; 38 | } else { 39 | link.href = identifier; 40 | 41 | let downloadName; 42 | if (media.length) { 43 | downloadName = media; 44 | } else { 45 | downloadName = JSINFO.id.split(':').pop() + `.${ext}`; 46 | } 47 | link.setAttribute('download', downloadName); 48 | button.append(LANG.plugins.diagrams.downloadSVGButtonShort); 49 | button.title = LANG.plugins.diagrams.downloadSVGButton; 50 | } 51 | 52 | link.appendChild(button); 53 | 54 | return link; 55 | } 56 | 57 | /** 58 | * HTML of an open button 59 | * 60 | * @param {string} url 61 | * @returns {HTMLButtonElement} 62 | */ 63 | static getOpenButton(url) { 64 | const button = document.createElement('button'); 65 | button.className = 'diagrams-btn'; 66 | button.innerText = LANG.plugins.diagrams.openButtonShort; 67 | button.title = LANG.plugins.diagrams.openButton; 68 | 69 | button.prepend(ButtonFunctions.getButtonIcon('open')); 70 | 71 | button.addEventListener('click', event => { 72 | event.preventDefault(); 73 | window.location = url; 74 | }); 75 | 76 | return button; 77 | } 78 | 79 | /** 80 | * Icon HTML 81 | * 82 | * @param {string} button 83 | * @returns {HTMLSpanElement} 84 | */ 85 | static getButtonIcon(button) { 86 | const icon = document.createElement('span'); 87 | icon.className = `icon-${button}`; 88 | 89 | switch (button) { 90 | case "open": 91 | icon.innerHTML = ''; 92 | break; 93 | case "edit": 94 | icon.innerHTML = ''; 95 | break; 96 | case "download": 97 | icon.innerHTML = ''; 98 | break; 99 | } 100 | 101 | return icon; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /script/DiagramsEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Callback for saving a diagram 3 | * @callback saveCallback 4 | * @param {string} svg The SVG data to save 5 | * @returns {Promise|boolean} true if saving was successful, false otherwise 6 | */ 7 | 8 | /** 9 | * Callback for when saving has finished suscessfully 10 | * @callback postSaveCallback 11 | */ 12 | 13 | 14 | /** 15 | * This class encapsulates all interaction with the diagrams editor 16 | * 17 | * It manages displaying and communicating with the editor, most importantly in manages loading 18 | * and saving diagrams. 19 | * 20 | * Note: devs should take care to ensure that only ever one instance of this class is active at a time 21 | * in the same window. 22 | * 23 | * FIXME we're not catching any fetch exceptions currently. Should we? 24 | * @class 25 | */ 26 | class DiagramsEditor { 27 | /** @type {HTMLIFrameElement} the editor iframe */ 28 | #diagramsEditor = null; 29 | 30 | /** @type {saveCallback} the method to call for saving the diagram */ 31 | #saveCallback = null; 32 | 33 | /** @type {postSaveCallback} called when saving has finished*/ 34 | #postSaveCallback = null; 35 | 36 | /** @type {string} the initial save data to load, set by one of the edit* methods */ 37 | #svg = ''; 38 | 39 | /** @type {function} the bound message listener */ 40 | #listener = null; 41 | 42 | /** 43 | * Create a new diagrams editor 44 | * 45 | * @param {postSaveCallback} postSaveCallback Called when saving has finished 46 | */ 47 | constructor(postSaveCallback = null) { 48 | this.#postSaveCallback = postSaveCallback; 49 | } 50 | 51 | /** 52 | * Initialize the editor for editing a media file 53 | * 54 | * @param {string} mediaid The media ID to edit, if 404 a new file will be created 55 | */ 56 | async editMediaFile(mediaid) { 57 | this.#saveCallback = (svg) => this.#saveMediaFile(mediaid, svg); 58 | 59 | const response = await fetch(DOKU_BASE + 'lib/exe/fetch.php?media=' + mediaid, { 60 | method: 'GET', 61 | cache: 'no-cache', 62 | }); 63 | 64 | if (response.ok) { 65 | // if not 404, load the SVG data 66 | this.#svg = await response.text(); 67 | } 68 | 69 | this.#createEditor(); 70 | } 71 | 72 | /** 73 | * Initialize the editor for editing an embedded diagram 74 | * 75 | * @param {string} pageid The page ID to on which the diagram is embedded 76 | * @param {int} position The position of the diagram in the page 77 | * @param {int} length The length of the diagram in the page 78 | */ 79 | async editEmbed(pageid, position, length) { 80 | this.#saveCallback = (svg) => this.#saveEmbed(pageid, position, length, svg); 81 | 82 | const url = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_embed_load' + 83 | '&id=' + encodeURIComponent(pageid) + 84 | '&pos=' + encodeURIComponent(position) + 85 | '&len=' + encodeURIComponent(length); 86 | 87 | const response = await fetch(url, { 88 | method: 'GET', 89 | cache: 'no-cache', 90 | }); 91 | 92 | if (response.ok) { 93 | // if not 404, load the SVG data 94 | this.#svg = await response.text(); 95 | } else { 96 | // a 404 for an embedded diagram should not happen 97 | alert(LANG.plugins.diagrams.errorLoading); 98 | return; 99 | } 100 | 101 | this.#createEditor(); 102 | } 103 | 104 | /** 105 | * Initialize the editor for editing a diagram in memory 106 | * 107 | * @param {string} svg The SVG raw data to edit, empty for new file 108 | * @param {saveCallback} callback The callback to call when the editor is closed 109 | */ 110 | editMemory(svg, callback) { 111 | this.#svg = svg; 112 | this.#saveCallback = callback.bind(this); 113 | this.#createEditor(); 114 | } 115 | 116 | /** 117 | * Saves a diagram as a media file 118 | * 119 | * @param {string} mediaid The media ID to save 120 | * @param {string} svg The SVG raw data to save 121 | * @returns {Promise} 122 | */ 123 | async #saveMediaFile(mediaid, svg) { 124 | const uploadUrl = this.#mediaUploadUrl(mediaid); 125 | 126 | const response = await fetch(uploadUrl, { 127 | method: 'POST', 128 | cache: 'no-cache', 129 | body: svg, 130 | }); 131 | 132 | return response.ok; 133 | } 134 | 135 | /** 136 | * Saves a diagram as an embedded diagram 137 | * 138 | * This replaces the previous diagram at the given postion 139 | * 140 | * @param {string} pageid The page ID on which the diagram is embedded 141 | * @param {int} position The position of the diagram in the page 142 | * @param {int} length The length of the diagram as it was before 143 | * @param {string} svg The SVG raw data to save 144 | * @returns {Promise} 145 | */ 146 | async #saveEmbed(pageid, position, length, svg) { 147 | const uploadUrl = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_embed_save' + 148 | '&id=' + encodeURIComponent(pageid) + 149 | '&pos=' + encodeURIComponent(position) + 150 | '&len=' + encodeURIComponent(length) + 151 | '§ok=' + JSINFO['sectok']; 152 | 153 | const body = new FormData(); 154 | body.set('svg', svg); 155 | 156 | const response = await fetch(uploadUrl, { 157 | method: 'POST', 158 | cache: 'no-cache', 159 | body: body, 160 | }); 161 | 162 | return response.ok; 163 | } 164 | 165 | /** 166 | * Save the PNG cache for a diagram 167 | * 168 | * @param {string} svg 169 | * @param {string} png 170 | * @returns {Promise} 171 | */ 172 | async #savePngCache(svg, png) { 173 | const uploadUrl = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_savecache' + 174 | '§ok=' + JSINFO['sectok']; 175 | 176 | const body = new FormData(); 177 | body.set('svg', svg); 178 | body.set('png', png); 179 | 180 | const response = await fetch(uploadUrl, { 181 | method: 'POST', 182 | cache: 'no-cache', 183 | body: body, 184 | }); 185 | 186 | return response.ok; 187 | } 188 | 189 | /** 190 | * Create the editor iframe and attach the message listener 191 | */ 192 | #createEditor() { 193 | this.#diagramsEditor = document.createElement('iframe'); 194 | this.#diagramsEditor.id = 'plugin__diagrams-editor'; 195 | this.#diagramsEditor.src = JSINFO['plugins']['diagrams']['service_url']; 196 | document.body.appendChild(this.#diagramsEditor); 197 | 198 | this.#listener = this.#handleMessage.bind(this); 199 | window.addEventListener('message', this.#listener); 200 | } 201 | 202 | /** 203 | * Remove the editor iframe and detach the message listener 204 | */ 205 | #removeEditor() { 206 | if (this.#diagramsEditor === null) return; 207 | this.#diagramsEditor.remove(); 208 | this.#diagramsEditor = null; 209 | window.removeEventListener('message', this.#listener); 210 | } 211 | 212 | /** 213 | * Get the raw data from a data URI 214 | * 215 | * @param {string} dataUri 216 | * @returns {string|null} 217 | */ 218 | #decodeDataUri(dataUri) { 219 | const matches = dataUri.match(/^data:(.*);base64,(.*)$/); 220 | if (matches === null) return null; 221 | 222 | return decodeURIComponent( 223 | atob(matches[2]) 224 | .split('') 225 | .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) 226 | .join('') 227 | ); 228 | } 229 | 230 | /** 231 | * Handle messages from diagramming service 232 | * 233 | * @param {Event} event 234 | */ 235 | async #handleMessage(event) { 236 | const msg = JSON.parse(event.data); 237 | 238 | switch (msg.event) { 239 | case 'init': 240 | // load the SVG data into the editor 241 | this.#diagramsEditor.contentWindow.postMessage(JSON.stringify({action: 'load', xml: this.#svg}), '*'); 242 | break; 243 | case 'save': 244 | this.#svg = ''; 245 | 246 | // Save triggers an export to SVG action 247 | this.#diagramsEditor.contentWindow.postMessage( 248 | JSON.stringify({ 249 | action: 'export', 250 | format: 'xmlsvg', 251 | spin: LANG.plugins.diagrams.saving 252 | }), 253 | '*' 254 | ); 255 | break; 256 | case 'export': 257 | if (msg.format === 'svg') { 258 | this.#svg = this.#decodeDataUri(msg.data); 259 | 260 | // export again as PNG 261 | this.#diagramsEditor.contentWindow.postMessage( 262 | JSON.stringify({ 263 | action: 'export', 264 | format: 'png', 265 | spin: LANG.plugins.diagrams.saving 266 | }), 267 | '*' 268 | ); 269 | } else if (msg.format === 'png') { 270 | const png = msg.data; // keep as data uri, for binary safety 271 | let ok = await this.#savePngCache(this.#svg, png); 272 | if (!ok) { 273 | alert(LANG.plugins.diagrams.errorSaving); 274 | return; 275 | } 276 | ok = await this.#saveCallback(this.#svg); 277 | if (ok) { 278 | this.#removeEditor(); 279 | if (this.#postSaveCallback !== null) { 280 | this.#postSaveCallback(); 281 | } 282 | } else { 283 | alert(LANG.plugins.diagrams.errorSaving); 284 | } 285 | } else { 286 | alert(LANG.plugins.diagrams.errorUnsupportedFormat); 287 | return; 288 | } 289 | break; 290 | case 'exit': 291 | this.#removeEditor(); 292 | break; 293 | } 294 | } 295 | 296 | /** 297 | * Get the URL to upload a media file 298 | * @param {string} mediaid 299 | * @returns {string} 300 | */ 301 | #mediaUploadUrl(mediaid) { 302 | // split mediaid into namespace and id 303 | let id = mediaid; 304 | let ns = ''; 305 | const idParts = id.split(':'); 306 | if (idParts.length > 1) { 307 | id = idParts.pop(idParts); 308 | ns = idParts.join(':'); 309 | } 310 | 311 | return DOKU_BASE + 312 | 'lib/exe/ajax.php?call=mediaupload&ow=true&ns=' + 313 | encodeURIComponent(ns) + 314 | '&qqfile=' + 315 | encodeURIComponent(id) + 316 | '§ok=' + 317 | encodeURIComponent(JSINFO['sectok']); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /script/DiagramsForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProseMirror Form for editing diagram attribute 3 | */ 4 | class DiagramsForm extends KeyValueForm { 5 | 6 | #attributes = { 7 | id: '', 8 | svg: '', 9 | type: '', 10 | title: '', 11 | url: '', 12 | width: null, 13 | height: null, 14 | align: '' 15 | }; 16 | 17 | #onsubmitCB = null; 18 | #oncloseCB = null; 19 | 20 | /** 21 | * Initialize the KeyValue form with fields and event handlers 22 | */ 23 | constructor(attributes, onsubmit, onclose = null) { 24 | const name = LANG.plugins.diagrams.formtitle; 25 | const fields = DiagramsForm.#getFields(attributes); 26 | 27 | super(name, fields); 28 | this.#attributes = { 29 | ...this.#attributes, 30 | ...attributes 31 | }; 32 | this.#onsubmitCB = onsubmit; 33 | this.#oncloseCB = onclose; 34 | 35 | // attach handlers 36 | this.$form.on('submit', (event) => { 37 | event.preventDefault(); // prevent form submission 38 | this.#onsubmitCB(this.#attributes); 39 | this.hide(); // close dialog 40 | }); 41 | 42 | 43 | this.$form.on('dialogclose', (event) => { 44 | if (this.#oncloseCB) this.#oncloseCB(); 45 | this.destroy(); 46 | }); 47 | 48 | this.$form.on('change', 'input,select', this.updateInternalState.bind(this)); 49 | 50 | this.#getButtonsMediaManager(this.#attributes); 51 | this.#getButtonsEditor(this.#attributes); 52 | 53 | this.updateFormState(); 54 | } 55 | 56 | #getButtonsEditor(attributes) { 57 | if (attributes.type === 'embed' || attributes.id) { 58 | const editButton = document.createElement('button'); 59 | editButton.className = 'diagrams-btn-edit'; 60 | editButton.id = 'diagrams__btn-edit'; 61 | editButton.innerText = LANG.plugins.diagrams.editButton; 62 | editButton.type = 'button'; 63 | this.$form.find('fieldset').append(editButton); 64 | 65 | editButton.addEventListener('click', event => { 66 | event.preventDefault(); // prevent form submission 67 | 68 | if (attributes.type === 'mediafile') { 69 | const diagramsEditor = new DiagramsEditor(this.onSavedMediaFile.bind(this, attributes.id)); 70 | diagramsEditor.editMediaFile(attributes.id); 71 | } else { 72 | const diagramsEditor = new DiagramsEditor(); 73 | diagramsEditor.editMemory(attributes.url, this.onSaveEmbed.bind(this)); 74 | } 75 | }); 76 | } 77 | } 78 | 79 | #getButtonsMediaManager(attributes) { 80 | // media manager button 81 | if (attributes.type === 'mediafile') { 82 | const selectButton = document.createElement('button'); 83 | selectButton.innerText = LANG.plugins.diagrams.selectSource; 84 | selectButton.className = 'diagrams-btn-select'; 85 | selectButton.type = 'button'; 86 | selectButton.addEventListener('click', () => 87 | window.open( 88 | `${DOKU_BASE}lib/exe/mediamanager.php?ns=${encodeURIComponent(JSINFO.namespace)}&onselect=dMediaSelect`, 89 | 'mediaselect', 90 | 'width=750,height=500,left=20,top=20,scrollbars=yes,resizable=yes', 91 | ) 92 | ); 93 | this.$form.find('fieldset').prepend(selectButton); 94 | window.dMediaSelect = this.mediaSelect.bind(this); // register as global function 95 | } 96 | } 97 | 98 | /** 99 | * Define form fields depending on type 100 | * @returns {object} 101 | */ 102 | static #getFields(attributes) { 103 | const fields = [ 104 | { 105 | type: 'select', 'label': LANG.plugins.diagrams.alignment, 'name': 'align', 'options': 106 | [ 107 | {value: '', label: ''}, 108 | {value: 'left', label: LANG.plugins.diagrams.left}, 109 | {value: 'right', label: LANG.plugins.diagrams.right}, 110 | {value: 'center', label: LANG.plugins.diagrams.center} 111 | ] 112 | }, 113 | { 114 | label: LANG.plugins.diagrams.title, name: 'title' 115 | } 116 | ]; 117 | 118 | if (attributes.type === 'mediafile') { 119 | fields.unshift( 120 | { 121 | label: LANG.plugins.diagrams.mediaSource, 122 | name: 'id' 123 | } 124 | ); 125 | } 126 | return fields; 127 | } 128 | 129 | /** 130 | * Updates the form to reflect the current internal attributes 131 | */ 132 | updateFormState() { 133 | for (const [key, value] of Object.entries(this.#attributes)) { 134 | this.$form.find('[name="' + key + '"]').val(value); 135 | } 136 | this.updateInternalUrl(); 137 | } 138 | 139 | /** 140 | * Update the internal attributes to reflect the current form state 141 | */ 142 | updateInternalState() { 143 | for (const [key, value] of Object.entries(this.#attributes)) { 144 | const $elem = this.$form.find('[name="' + key + '"]'); 145 | if ($elem.length) { 146 | this.#attributes[key] = $elem.val(); 147 | } 148 | } 149 | this.updateInternalUrl(); 150 | } 151 | 152 | /** 153 | * Calculate the Display URL for the current mediafile 154 | */ 155 | updateInternalUrl() { 156 | if (this.#attributes.type === 'mediafile') { 157 | this.#attributes.url = `${DOKU_BASE}lib/exe/fetch.php?media=${this.#attributes.id}`; 158 | } 159 | } 160 | 161 | /** 162 | * After svaing a media file reload the src for all images using it 163 | * 164 | * @see https://stackoverflow.com/a/66312176 165 | * @param {string} mediaid 166 | * @returns {Promise} 167 | */ 168 | async onSavedMediaFile(mediaid) { 169 | const url = `${DOKU_BASE}lib/exe/fetch.php?cache=nocache&media=${mediaid}`; 170 | await fetch(url, {cache: 'reload', mode: 'no-cors'}); 171 | document.body.querySelectorAll(`img[src='${url}']`) 172 | .forEach(img => img.src = url) 173 | } 174 | 175 | /** 176 | * Save an embedded diagram back to the editor 177 | */ 178 | onSaveEmbed(svg) { 179 | const encSvg = this.bytesToBase64(new TextEncoder().encode(svg)); 180 | this.#attributes.url = 'data:image/svg+xml;base64,' + encSvg; 181 | this.updateFormState(); 182 | return true; 183 | } 184 | 185 | /** 186 | * Callback called from the media popup on selecting a file 187 | * 188 | * This is globally registered as window.dMediaSelect 189 | * 190 | * @param {string} edid ignored 191 | * @param {string} mediaid the picked media ID 192 | */ 193 | async mediaSelect(edid, mediaid) { 194 | const response = await fetch( 195 | `${DOKU_BASE}lib/exe/ajax.php?call=plugin_diagrams_mediafile_isdiagramcheck&diagram=` + 196 | encodeURIComponent(mediaid), 197 | { 198 | method: 'POST', 199 | cache: 'no-cache', 200 | } 201 | ); 202 | 203 | if (!response.ok) { 204 | alert(LANG.plugins.diagrams.mediafileIsNotDiagram); 205 | return; 206 | } 207 | 208 | this.#attributes.id = mediaid; 209 | this.updateFormState(); 210 | } 211 | 212 | /** 213 | * UTF-8 safe Base64 encoder 214 | * 215 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem 216 | * @param bytes 217 | * @returns {string} 218 | */ 219 | bytesToBase64(bytes) { 220 | const binString = String.fromCodePoint(...bytes); 221 | return btoa(binString); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /script/DiagramsMediaManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attaches diagram functionality in the MediaManager and MediaPopup 3 | */ 4 | class DiagramsMediaManager { 5 | 6 | /** 7 | * Attach the handlers 8 | */ 9 | constructor() { 10 | const tree = document.querySelector('#media__tree'); 11 | if (tree) { 12 | const createLink = document.createElement('a'); 13 | createLink.addEventListener('click', this.#showCreationDialog.bind(this)); 14 | createLink.className = 'plugin_diagrams_create'; 15 | createLink.innerText = LANG.plugins.diagrams.createLink; 16 | createLink.href = '#'; 17 | tree.prepend(createLink); 18 | } 19 | 20 | const filePanel = document.querySelector('#mediamanager__page .panel.file'); 21 | if (filePanel) { 22 | const observer = new MutationObserver(this.#addEditButton); 23 | observer.observe(filePanel, {childList: true, subtree: true}); 24 | } 25 | } 26 | 27 | /** 28 | * Observer callback to add the edit button in the detail panel of the media manager 29 | * 30 | * @param mutationsList 31 | * @param observer 32 | */ 33 | async #addEditButton(mutationsList, observer) { 34 | for (let mutation of mutationsList) { 35 | // div.file has been filled with new content? 36 | if (mutation.type !== 'childList') continue; 37 | 38 | // is it an SVG file? 39 | const svgLink = mutation.target.querySelector('a.mf_svg'); 40 | if (!svgLink) continue; 41 | 42 | const actionList = mutation.target.querySelector('ul.actions'); 43 | if (actionList.querySelector('button.diagrams-btn')) continue; // already added 44 | 45 | // ensure media file is actually an editable diagram 46 | const response = await fetch( 47 | DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_mediafile_editcheck&diagrams=' + 48 | encodeURIComponent(JSON.stringify([svgLink.textContent])), 49 | { 50 | method: 'GET', 51 | cache: 'no-cache', 52 | } 53 | ); 54 | 55 | if (response.ok && (await response.json())[0] === svgLink.textContent) { 56 | const editButton = document.createElement('button'); 57 | editButton.classList.add('diagrams-btn'); 58 | editButton.innerText = LANG.plugins.diagrams.editButton; 59 | editButton.addEventListener('click', async () => { 60 | const editor = new DiagramsEditor(); 61 | await editor.editMediaFile(svgLink.textContent); 62 | }); 63 | actionList.appendChild(editButton); 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Show the dialog to create a new diagram 70 | * 71 | * Uses JQuery UI 72 | * 73 | * @param {Event} event 74 | * @returns {Promise} 75 | */ 76 | async #showCreationDialog(event) { 77 | event.preventDefault(); 78 | event.stopPropagation(); 79 | const namespace = this.#getNamespace(); 80 | 81 | if (!await this.#checkACLs(namespace)) { 82 | alert(LANG.plugins.diagrams.createForbidden); 83 | return; 84 | } 85 | 86 | const $form = jQuery(this.#buildForm(namespace)); 87 | $form.dialog({ 88 | title: LANG.plugins.diagrams.createLink, 89 | width: 600, 90 | appendTo: '.dokuwiki', 91 | modal: true, 92 | close: function () { 93 | // do not reuse the dialog 94 | // https://stackoverflow.com/a/2864783 95 | jQuery(this).dialog('destroy').remove(); 96 | } 97 | }); 98 | } 99 | 100 | /** 101 | * Check if the user has the right to create a diagram in the given namespace 102 | * 103 | * @param {string} namespace 104 | * @returns {Promise} 105 | */ 106 | async #checkACLs(namespace) { 107 | const url = DOKU_BASE + 'lib/exe/ajax.php' + 108 | '?call=plugin_diagrams_mediafile_nscheck' + 109 | '&ns=' + encodeURIComponent(namespace); 110 | 111 | 112 | const response = await fetch(url, { 113 | cache: 'no-cache', 114 | }); 115 | 116 | return response.json(); 117 | } 118 | 119 | /** 120 | * Extract the namespace from the page 121 | * 122 | * @returns {string} 123 | */ 124 | #getNamespace() { 125 | const fullScreenNS = document.querySelector('#mediamanager__page .panelHeader h3 strong'); 126 | const popupNS = document.querySelector('#media__manager #media__ns'); 127 | 128 | let namespace = ''; 129 | if (fullScreenNS) { 130 | namespace = fullScreenNS.textContent; 131 | } else if (popupNS) { 132 | namespace = popupNS.textContent; 133 | } else { 134 | throw new Error('Could not find namespace'); //should not happen 135 | } 136 | 137 | // Media Manager encloses the root dir in [] so let's strip that 138 | // because it is not a real namespace 139 | return namespace.replace(/^:|\[.*\]$/, ''); 140 | } 141 | 142 | /** 143 | * Create the form to ask for the diagram name 144 | * @param ns 145 | * @returns {HTMLDivElement} 146 | */ 147 | #buildForm(ns) { 148 | const wrapper = document.createElement('div'); 149 | const form = document.createElement('form'); 150 | wrapper.appendChild(form); 151 | 152 | const intro = document.createElement('p'); 153 | intro.innerText = LANG.plugins.diagrams.createIntro; 154 | const namespace = document.createElement('strong'); 155 | namespace.innerText = ':'+ns; 156 | intro.appendChild(namespace); 157 | form.appendChild(intro); 158 | 159 | const input = document.createElement('input'); 160 | input.type = 'text'; 161 | input.className = 'edit'; 162 | input.name = 'diagrams-create-filename'; 163 | form.appendChild(input); 164 | 165 | const button = document.createElement('button'); 166 | button.innerText = LANG.plugins.diagrams.createButton; 167 | form.appendChild(button); 168 | 169 | form.addEventListener('submit', this.#createDiagram.bind(this, ns, input)); 170 | return wrapper; 171 | } 172 | 173 | /** 174 | * Open the diagram editor for the given namespace and filename 175 | * 176 | * @param {string} namespace The current namespace 177 | * @param {HTMLInputElement} input The input element containing the filename 178 | * @param {Event} event 179 | */ 180 | async #createDiagram(namespace, input, event) { 181 | event.preventDefault(); 182 | event.stopPropagation(); 183 | 184 | const id = input.value; 185 | 186 | // check for validity 187 | if (id.length < 0 || !/^[\w][\w\.\-]*$/.test(id)) { 188 | alert(LANG.plugins.diagrams.errorInvalidId); 189 | return; 190 | } 191 | const svg = namespace + ':' + id + '.svg'; 192 | 193 | if (await this.#checkOverwrite(svg)) { 194 | alert('File exists already! Choose a different name!'); 195 | return; 196 | } 197 | 198 | const editor = new DiagramsEditor(() => { 199 | let url = new URL(window.location.href); 200 | url.searchParams.set('ns', namespace); 201 | // these will be ignored in the popup: 202 | url.searchParams.set('image', svg); 203 | url.searchParams.set('tab_details', 'view'); 204 | url.searchParams.set('tab_files', 'files'); 205 | window.location.href = url.toString(); 206 | }); 207 | editor.editMediaFile(svg); 208 | } 209 | 210 | /** 211 | * Check with backend if a file with the given name already exists 212 | * 213 | * @param {string} svg 214 | * @returns {Promise} 215 | */ 216 | async #checkOverwrite(svg) { 217 | const url = DOKU_BASE + 'lib/exe/ajax.php' + 218 | '?call=plugin_diagrams_mediafile_existscheck' + 219 | '&mediaId=' + encodeURIComponent(svg); 220 | 221 | const response = await fetch(url, { 222 | cache: 'no-cache', 223 | }); 224 | 225 | return response.json(); 226 | } 227 | } 228 | 229 | // initialize 230 | document.addEventListener('DOMContentLoaded', () => { 231 | new DiagramsMediaManager(); 232 | }); 233 | -------------------------------------------------------------------------------- /script/DiagramsMenuItemDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a menu entry to insert a new mediafile diagram 3 | */ 4 | class DiagramsMenuItemDispatcherMediaFile extends AbstractMenuItemDispatcher { 5 | /** The type of the node to be inserted */ 6 | static type = 'mediafile'; 7 | 8 | /** 9 | * Check if the schema is available 10 | * 11 | * @param schema 12 | * @returns {boolean} 13 | */ 14 | static isAvailable(schema) { 15 | return !!schema.nodes.diagrams; 16 | } 17 | 18 | /** 19 | * Get the menu icon 20 | * 21 | * @todo the inline styles here should be part of the prosemirror plugin default styles 22 | * @returns {HTMLSpanElement} 23 | */ 24 | static getIcon() { 25 | const svgIcon = document.createElement('img'); 26 | svgIcon.src = DOKU_BASE + 'lib/plugins/diagrams/img/diagramsnet.svg'; 27 | svgIcon.style.width = '1.2em'; 28 | svgIcon.style.height = '1.2em'; 29 | svgIcon.style.float = 'none'; 30 | 31 | const wrapper = document.createElement('span'); 32 | wrapper.appendChild(svgIcon); 33 | wrapper.className = 'menuicon'; 34 | 35 | return wrapper; 36 | } 37 | 38 | /** 39 | * Return the menu item 40 | * 41 | * @param schema 42 | * @returns {MenuItem} 43 | */ 44 | static getMenuItem(schema) { 45 | if (!this.isAvailable(schema)) { 46 | throw new Error('Diagrams is missing in provided Schema!'); 47 | } 48 | 49 | return new MenuItem({ 50 | command: (state, dispatch) => { 51 | const {$from} = state.selection; 52 | const index = $from.index(); 53 | if (!$from.parent.canReplaceWith(index, index, schema.nodes.diagrams)) { 54 | return false; 55 | } 56 | if (dispatch) { 57 | let textContent = ''; 58 | state.selection.content().content.descendants((node) => { 59 | textContent += node.textContent; 60 | return false; 61 | }); 62 | 63 | 64 | const dForm = new DiagramsForm( 65 | { 66 | title: textContent ? textContent : '', 67 | type: this.type, 68 | }, 69 | (attributes) => { 70 | dispatch( 71 | state.tr.replaceSelectionWith( 72 | schema.nodes.diagrams.create(attributes) 73 | ) 74 | ) 75 | } 76 | ); 77 | dForm.show(); 78 | } 79 | return true; 80 | }, 81 | icon: this.getIcon(), 82 | label: LANG.plugins.diagrams['PMMenuItem-' + this.type], 83 | }); 84 | } 85 | } 86 | 87 | /** 88 | * Creates a menu entry to insert a new embedded diagram 89 | */ 90 | class DiagramsMenuItemDispatcherEmbedded extends DiagramsMenuItemDispatcherMediaFile { 91 | static type = 'embed'; 92 | } 93 | -------------------------------------------------------------------------------- /script/DiagramsView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prosemirror view for the diagrams node 3 | */ 4 | class DiagramsView extends AbstractNodeView { 5 | /** {DiagramsForm} The form to edit the node attributes */ 6 | #dForm = null; 7 | 8 | 9 | /** 10 | * Render the node into this.dom 11 | * 12 | * We use the schema's spec.toDOM() method instead of directly using the passed attributes 13 | * to render the node to avoid code duplication and to have a central way to map node 14 | * attributes to dom attributes. 15 | * 16 | * @param {object} attrs 17 | */ 18 | renderNode(attrs) { 19 | const schemaSpec = this.node.type.spec.toDOM(this.node); 20 | const elem = document.createElement(schemaSpec[0]); 21 | 22 | // copy attributes to dom element 23 | Object.entries(schemaSpec[1]).forEach(([key, value]) => { 24 | if (value) { 25 | elem.setAttribute(key, value); 26 | } 27 | }); 28 | 29 | this.dom = elem; 30 | } 31 | 32 | /** 33 | * Handle node selection 34 | * 35 | * Update and show the form 36 | */ 37 | selectNode() { 38 | this.dom.classList.add('ProseMirror-selectednode'); 39 | 40 | this.#dForm = new DiagramsForm( 41 | this.node.attrs, 42 | this.dispatchNodeUpdate.bind(this), 43 | this.deselectNode.bind(this) 44 | ); 45 | this.#dForm.show(); 46 | } 47 | 48 | /** 49 | * Handle node deselection 50 | * 51 | * Closes the form 52 | */ 53 | deselectNode() { 54 | this.dom.classList.remove('ProseMirror-selectednode'); 55 | } 56 | 57 | /** 58 | * Dispatches a node update to the editor 59 | * 60 | * @param {object} newAttrs 61 | */ 62 | dispatchNodeUpdate(newAttrs) { 63 | const nodeStartPos = this.getPos(); 64 | this.outerView.dispatch(this.outerView.state.tr.setNodeMarkup( 65 | nodeStartPos, 66 | null, 67 | newAttrs, 68 | this.node.marks, 69 | )); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /script/download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach download and open buttons to diagrams 3 | */ 4 | document.addEventListener('DOMContentLoaded', () => { 5 | 6 | document.querySelectorAll('div.diagrams-buttons').forEach(diagramActions => { 7 | const $diagram = jQuery(diagramActions.parentNode.querySelector('object.diagrams-svg')); 8 | const url = $diagram.attr('data'); 9 | const pngcache = $diagram.data('pngcache'); 10 | 11 | // media files have an id, embedded diagrams don't 12 | let media = ''; 13 | if (typeof $diagram.data('id') !== "undefined") { 14 | media = $diagram.data('id'); 15 | } 16 | 17 | // download 18 | diagramActions.prepend(ButtonFunctions.getDownloadButton('svg', url, media)); 19 | if (pngcache) { 20 | diagramActions.prepend(ButtonFunctions.getDownloadButton('png', pngcache, media)); 21 | } 22 | 23 | // open 24 | diagramActions.prepend(ButtonFunctions.getOpenButton(url)); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /script/embed-editbutton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach editing button to editable diagrams 3 | */ 4 | document.addEventListener('DOMContentLoaded', () => { 5 | // check if the current page is editable by the current user 6 | if (!document.querySelector('head link[rel="edit"]')) return; 7 | 8 | document.querySelectorAll('object.diagrams-svg[data-pos]').forEach(embed => { 9 | const button = document.createElement('button'); 10 | button.className = 'diagrams-btn'; 11 | button.innerText = LANG.plugins.diagrams.editButtonShort; 12 | button.title = LANG.plugins.diagrams.editButton; 13 | 14 | const icon = ButtonFunctions.getButtonIcon('edit'); 15 | button.prepend(icon); 16 | 17 | button.addEventListener('click', event => { 18 | event.preventDefault(); 19 | const diagramsEditor = new DiagramsEditor(() => { 20 | // replace instead of reload to avoid accidentally re-submitting forms 21 | window.location.replace(window.location.href); 22 | }); 23 | diagramsEditor.editEmbed( 24 | JSINFO.id, 25 | parseInt(embed.getAttribute('data-pos')), 26 | parseInt(embed.getAttribute('data-len')) 27 | ); 28 | }); 29 | 30 | embed.parentNode.querySelector('.diagrams-buttons').appendChild(button); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /script/embed-toolbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Integrate diagrams editing into the editor 3 | * 4 | * This requires some global code that's executed early (not in document.ready) 5 | */ 6 | if (typeof window.toolbar !== 'undefined') { 7 | /** 8 | * Select the diagram at the current cursor position 9 | * 10 | * @param area 11 | * @returns {selection_class} 12 | */ 13 | function selectDiagram(area) { 14 | const selection = DWgetSelection(area); 15 | const open = ' -1 && prev > min && start < prev) return selection; 34 | if (end > max) return selection; 35 | if (next > -1 && next < end && end > next) return selection; 36 | 37 | // still here? we are inside a boundary, new selection 38 | selection.start = area.value.indexOf('>', start) + 1; 39 | selection.end = end; 40 | DWsetSelection(selection); 41 | return selection; 42 | } 43 | 44 | function addBtnActionDiagramsPlugin($btn, props, edid) { 45 | $btn.on('click', function (e) { 46 | e.preventDefault(); 47 | const diagramsEditor = new DiagramsEditor(); 48 | 49 | const area = document.getElementById(edid); 50 | const selection = selectDiagram(area); 51 | 52 | const origSvg = area.value.substring(selection.start, selection.end); 53 | 54 | diagramsEditor.editMemory(origSvg, svg => { 55 | if (!origSvg) { 56 | // if this is a new diagram, wrap it in a tag 57 | svg = '' + svg + ''; 58 | } 59 | area.value = area.value.substring(0, selection.start) + 60 | svg + 61 | area.value.substring(selection.end, area.value.length); 62 | return true; 63 | }); 64 | 65 | 66 | }); 67 | } 68 | 69 | toolbar[toolbar.length] = { 70 | type: "DiagramsPlugin", 71 | title: LANG.plugins.diagrams.toolbarButton, 72 | icon: "../../plugins/diagrams/img/diagramsnet.png", 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /script/mediafile-editbutton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach editing button to media file diagrams in pages 3 | */ 4 | document.addEventListener('DOMContentLoaded', async () => { 5 | 6 | // get all diagrams images and their IDs 7 | const diagrams = document.querySelectorAll('object.diagrams-svg[data-id]'); 8 | const diagramIDs = Array.from(diagrams).map(image => image.getAttribute('data-id')); 9 | 10 | // check which of the found diagrams are editable 11 | const body = new FormData(); 12 | body.set('diagrams', JSON.stringify(diagramIDs)); 13 | const result = await fetch(DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_mediafile_editcheck', { 14 | method: 'POST', 15 | cache: 'no-cache', 16 | body: body, 17 | }); 18 | const editableDiagrams = await result.json(); 19 | 20 | // add edit button to editable diagrams 21 | diagrams.forEach(image => { 22 | if (editableDiagrams.includes(image.getAttribute('data-id'))) { 23 | const button = document.createElement('button'); 24 | button.className = 'diagrams-btn'; 25 | button.innerText = LANG.plugins.diagrams.editButtonShort; 26 | button.title = LANG.plugins.diagrams.editButton; 27 | 28 | const icon = ButtonFunctions.getButtonIcon('edit'); 29 | button.prepend(icon); 30 | 31 | button.addEventListener('click', event => { 32 | event.preventDefault(); 33 | const diagramsEditor = new DiagramsEditor(() => { 34 | // replace instead of reload to avoid accidentally re-submitting forms 35 | window.location.replace(window.location.href); 36 | }); 37 | diagramsEditor.editMediaFile(image.getAttribute('data-id')); 38 | }); 39 | image.parentNode.querySelector('.diagrams-buttons').appendChild(button); 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /script/mediafile-linkfix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Open links in diagrams in the browser window instead of SVG frame 3 | */ 4 | window.addEventListener('load', function () { 5 | /** 6 | * Sets _top target for links within SVG 7 | */ 8 | function manipulateLinkTarget() { 9 | jQuery('object.diagrams-svg').each(function () { 10 | jQuery(this.contentDocument).find('svg a').not('[target]').attr('target', '_top'); 11 | }); 12 | } 13 | 14 | /* template agnostic selector */ 15 | const observable = document.querySelector('body'); 16 | 17 | const bodyObserver = new MutationObserver((mutationsList, observer) => { 18 | for (const mutation of mutationsList) { 19 | if (mutation.type !== 'childList') continue; 20 | 21 | const nodes = mutation.addedNodes; 22 | 23 | nodes.forEach(node => { 24 | jQuery(node).find('object.diagrams-svg').each(function () { 25 | // SVG clicks do not bubble up, so we use an available event 26 | jQuery(this).on('load', manipulateLinkTarget); 27 | }); 28 | }); 29 | } 30 | }); 31 | 32 | /* rewrite link targets when document is initially loaded */ 33 | manipulateLinkTarget(); 34 | 35 | /** 36 | * Observe DOM changes 37 | * 38 | * FIXME this should no longer be necessary after Jack 39 | * @see https://github.com/dokuwiki/dokuwiki/pull/3957 40 | */ 41 | bodyObserver.observe(observable, { 42 | attributes: true, 43 | childList: true, 44 | subtree: true } 45 | ); 46 | }); 47 | -------------------------------------------------------------------------------- /script/prosemirror.js: -------------------------------------------------------------------------------- 1 | jQuery(document).on('PROSEMIRROR_API_INITIALIZED', () => { 2 | // define diagrams schema 3 | window.Prosemirror.pluginSchemas.push((nodes, marks) => { 4 | nodes = nodes.addToEnd('diagrams', { 5 | inline: true, 6 | selectable: true, 7 | attrs: { 8 | url: {}, 9 | id: {}, 10 | type: {default: 'mediafile'}, 11 | title: {default: null}, 12 | width: {default: null}, 13 | height: {default: null}, 14 | align: {default: ''} 15 | }, 16 | group: "inline", 17 | draggable: true, 18 | 19 | /** 20 | * Render the node as HTML 21 | * 22 | * Maps node attributes to HTML attributes 23 | * 24 | * @param node 25 | * @returns {[string,object]} [tagname, attributes] 26 | */ 27 | toDOM: function toDOM(node) { 28 | let alignclass = node.attrs.align; 29 | if (alignclass.length !== 0) { 30 | alignclass = ` media${alignclass}`; 31 | } 32 | 33 | return [ 34 | 'img', 35 | { 36 | class: 'media diagrams-svg' + alignclass, 37 | title: node.attrs.title, 38 | src: node.attrs.url, 39 | 'data-id': node.attrs.id, 40 | 'data-type': node.attrs.type, 41 | width: node.attrs.width, 42 | height: node.attrs.height, 43 | } 44 | ] 45 | } 46 | }); 47 | return {nodes, marks}; 48 | }); 49 | 50 | // extend plugin menu 51 | const AbstractMenuItemDispatcher = window.Prosemirror.classes.AbstractMenuItemDispatcher; 52 | const MenuItem = window.Prosemirror.classes.MenuItem; 53 | const KeyValueForm = window.Prosemirror.classes.KeyValueForm; 54 | const AbstractNodeView = window.AbstractNodeView; // FIXME this should be moved to the prosemirror.classes namespace 55 | 56 | /* DOKUWIKI:include script/DiagramsForm.js */ 57 | /* DOKUWIKI:include script/DiagramsView.js */ 58 | /* DOKUWIKI:include script/DiagramsMenuItemDispatcher.js */ 59 | 60 | 61 | window.Prosemirror.pluginNodeViews.diagrams = function diagrams(node, outerview, getPos) { 62 | return new DiagramsView(node, outerview, getPos); 63 | }; 64 | 65 | // noinspection JSBitwiseOperatorUsage 66 | if (JSINFO.plugins.diagrams && (JSINFO.plugins.diagrams.mode & 1)) { 67 | window.Prosemirror.pluginMenuItemDispatchers.push(DiagramsMenuItemDispatcherMediaFile); 68 | } 69 | 70 | // noinspection JSBitwiseOperatorUsage 71 | if (JSINFO.plugins.diagrams && (JSINFO.plugins.diagrams.mode & 2)) { 72 | window.Prosemirror.pluginMenuItemDispatchers.push(DiagramsMenuItemDispatcherEmbedded); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /svg.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /syntax/embed.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class syntax_plugin_diagrams_embed extends syntax_plugin_diagrams_mediafile 12 | { 13 | /** @var int count the current embedded diagram */ 14 | protected $count = 0; 15 | 16 | /** @inheritDoc */ 17 | public function connectTo($mode) 18 | { 19 | // only register if embed mode is enabled 20 | if (!($this->getConf('mode') & Diagrams::MODE_EMBED)) return; 21 | $this->Lexer->addSpecialPattern('.*?(?:)', $mode, 'plugin_diagrams_embed'); 22 | } 23 | 24 | /** @inheritDoc */ 25 | public function handle($match, $state, $pos, Doku_Handler $handler) 26 | { 27 | [$open, $rest] = sexplode('>', $match, 2); 28 | $params = substr($open, 9); 29 | $svg = substr($rest, 0, -10); 30 | 31 | // embed positions 32 | $svglen = strlen($svg); 33 | $svgpos = $pos + strpos($match, '>'); 34 | 35 | /** @var helper_plugin_diagrams $helper */ 36 | $helper = plugin_load('helper', 'diagrams'); 37 | if (!$helper->isDiagram($svg)) return false; 38 | 39 | $data = [ 40 | 'svg' => $svg, 41 | 'title' => '', 42 | 'align' => '', 43 | 'width' => '', 44 | 'height' => '', 45 | 'pos' => $svgpos, 46 | 'len' => $svglen, 47 | ]; 48 | 49 | [$params, $data['title']] = sexplode('|', $params, 2); 50 | 51 | if (preg_match('/\b(left|right|center)\b/', $params, $matches)) { 52 | $data['align'] = $matches[1]; 53 | } 54 | 55 | if (preg_match('/\b(\d+)x(\d+)\b/', $params, $matches)) { 56 | $data['width'] = (int)$matches[1]; 57 | $data['height'] = (int)$matches[2]; 58 | } 59 | 60 | return $data; 61 | } 62 | 63 | /** @inheritDoc */ 64 | public function render($format, Doku_Renderer $renderer, $data) 65 | { 66 | if (!$data) return false; 67 | global $ID; 68 | global $ACT; 69 | global $INPUT; 70 | global $REV; 71 | 72 | switch ($format) { 73 | case 'xhtml': 74 | if (act_clean($ACT) !== 'preview' && page_exists($ID)) { 75 | // this is "normal" rendering, we reference the diagram through the export 76 | // this applies the same CSP as media files and will also show the exact behaviours 77 | $data['url'] = wl($ID, ['do' => 'export_diagrams', 'svg' => $this->count++, 'rev' => $REV], true, '&'); 78 | } else { 79 | // we're in preview and the diagram may not have been saved, yet. So we 80 | // reference it as data uri to prevent cross-origin access XSS 81 | $data['url'] = 'data:image/svg+xml;base64,' . base64_encode($data['svg']); 82 | } 83 | 84 | parent::render($format, $renderer, $data); 85 | return true; 86 | case 'diagrams': 87 | // This exports a single SVG during the export_diagrams action 88 | if ($INPUT->int('svg') === $this->count++) { 89 | $renderer->doc = $data['svg']; 90 | } 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /syntax/mediafile.php: -------------------------------------------------------------------------------- 1 | getConf('mode') & Diagrams::MODE_MEDIA)) return; 34 | 35 | // grab all SVG images 36 | $this->Lexer->addSpecialPattern('\{\{[^\}]+(?:\.svg)[^\}]*?\}\}', $mode, 'plugin_diagrams_mediafile'); 37 | } 38 | 39 | /** 40 | * Parse SVG syntax into media data 41 | * 42 | * @param string $match 43 | * @param int $state 44 | * @param int $pos 45 | * @param Doku_Handler $handler 46 | * @return array|bool 47 | */ 48 | public function handle($match, $state, $pos, Doku_Handler $handler) 49 | { 50 | $data = Doku_Handler_Parse_Media($match); 51 | 52 | /** @var helper_plugin_diagrams $helper */ 53 | $helper = plugin_load('helper', 'diagrams'); 54 | if (!$data['type'] == 'internalmedia' || !$helper->isDiagramFile(mediaFN($data['src']))) { 55 | // This is not a local diagrams file, but some other SVG media file 56 | $handler->media($match, $state, $pos); 57 | return false; 58 | } 59 | 60 | $data['url'] = ml($data['src'], ['cache' => 'nocache'], true, '&'); 61 | return $data; 62 | } 63 | 64 | /** 65 | * Handle rewrites made by the move plugin 66 | * 67 | * @param string $match 68 | * @param int $state 69 | * @param int $pos 70 | * @param string $plugin 71 | * @param helper_plugin_move_handler $handler 72 | * @return void 73 | */ 74 | public function handleMove($match, $state, $pos, $plugin, $handler) 75 | { 76 | if ($plugin !== 'diagrams_mediafile') return; 77 | 78 | $handler->media($match, $state, $pos); 79 | } 80 | 81 | /** 82 | * Render the diagram SVG as instead of to allow links, 83 | * except when rendering to a PDF 84 | * 85 | * @param string $format 86 | * @param Doku_Renderer $renderer 87 | * @param array $data 88 | * @return bool 89 | */ 90 | public function render($format, Doku_Renderer $renderer, $data) 91 | { 92 | global $conf; 93 | 94 | if ($format === 'metadata') { 95 | $renderer->internalmedia($data['src']); 96 | return true; 97 | } 98 | if ($format !== 'xhtml') { 99 | return false; 100 | } 101 | 102 | // check for cached PNG 103 | $cachefile = $this->getCachedPNG($data); 104 | 105 | if (is_a($renderer, 'renderer_plugin_dw2pdf')) { 106 | $imageAttributes = [ 107 | 'class' => 'media', 108 | 'width' => empty($data['width']) ? '' : $data['width'], 109 | 'height' => empty($data['height']) ? '' : $data['height'], 110 | 'title' => $data['title'] ?? '', 111 | 'alt' => $data['title'] ?? '', 112 | 'align' => $data['align'], 113 | 'src' => $data['url'], 114 | ]; 115 | 116 | // if a PNG cache exists, use it instead of the real URL 117 | if ($cachefile) { 118 | $imageAttributes['src'] = 'dw2pdf://' . $cachefile; 119 | } 120 | 121 | $renderer->doc .= ''; 122 | } else { 123 | $wrapperAttributes = []; 124 | $wrapperAttributes['title'] = $data['title'] ?? ''; 125 | $wrapperAttributes['class'] = 'media diagrams-svg-wrapper media' . $data['align']; 126 | 127 | $imageAttributes = []; 128 | $imageAttributes['class'] = 'diagrams-svg'; 129 | $imageAttributes['data'] = $data['url']; 130 | $imageAttributes['data-id'] = cleanID($data['src'] ?? ''); 131 | $imageAttributes['type'] = 'image/svg+xml'; 132 | $imageAttributes['data-pos'] = $data['pos'] ?? ''; 133 | $imageAttributes['data-len'] = $data['len'] ?? ''; 134 | $imageAttributes['width'] = empty($data['width']) ? '' : $data['width']; 135 | $imageAttributes['height'] = empty($data['height']) ? '' : $data['height']; 136 | 137 | if ($cachefile) { 138 | // strip cache dir and our cache extension from data attribute 139 | $imageAttributes['data-pngcache'] = str_replace([$conf['cachedir'], Diagrams::CACHE_EXT], '', $cachefile); 140 | } 141 | 142 | $image = sprintf('' . hsc($wrapperAttributes['title']) . '', buildAttributes($imageAttributes, true)); 143 | // wrapper for action buttons 144 | $actionButtons = '
'; 145 | $wrapper = sprintf('
%s%s
', buildAttributes($wrapperAttributes, true), $image, $actionButtons); 146 | $renderer->doc .= $wrapper; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * PNG cache file without extension, if caching is enabled and file exists. 154 | * Returns an empty string on older revisions (checking $REV), because 155 | * PNG caching does not support versioning. 156 | * 157 | * @param array $data 158 | * @return string 159 | */ 160 | protected function getCachedPNG($data) 161 | { 162 | global $REV; 163 | 164 | if (!$this->getConf('pngcache') || $REV) return ''; 165 | 166 | if (empty($data['svg'])) { 167 | $data['svg'] = file_get_contents(mediaFN($data['src'])); 168 | } 169 | $cachefile = getCacheName($data['svg'], Diagrams::CACHE_EXT); 170 | if (file_exists($cachefile)) return $cachefile; 171 | 172 | return ''; 173 | } 174 | } 175 | --------------------------------------------------------------------------------