1# The fileedit Page 2 3This document describes the limitations of, caveats for, and 4disclaimers for the [](/fileedit) page, which provides users with 5[checkin privileges](./caps/index.md) basic editing features for files 6via the web interface. 7 8# Important Caveats and Disclaimers 9 10Predictably, the ability to edit files in a repository from a web 11browser halfway around the world comes with several obligatory caveats 12and disclaimers... 13 14## <a id="cap"></a> `/fileedit` Does *Nothing* by Default. 15 16In order to "activate" it, a user with [the "setup" 17permission](./caps/index.md) must set the 18[fileedit-glob](/help?cmd=fileedit-glob) repository setting to a 19comma- or newline-delimited list of globs representing a whitelist of 20files which may be edited online. Any user with commit access may then 21edit files matching one of those globs. Certain pages within the UI 22get an "edit" link added to them when the current user's permissions 23and the whitelist both permit editing of that file. 24 25## <a id="csrf"></a> CSRF & HTTP Referrer Headers 26 27In order to protect against [Cross-site Request Forgery (CSRF)][csrf] 28attacks, Fossil UI features which write to the database require that 29the browser send the so-called [HTTP `Referer` header][referer] 30(noting that the misspelling of "referrer" is a historical accident 31which has long-since been standardized!). Modern browsers, by default, 32include such information automatically for *interactive* actions which 33lead to a request, e.g. clicking on a link back to the same 34server. However, `/fileedit` uses asynchronous ["XHR"][xhr] 35connections, which browsers *may* treat differently than strictly 36interactive elements. 37 38- **Firefox**: configuration option `network.http.sendRefererHeader` 39 specifies whether the `Referer` header is sent. It must have a value 40 of 2 (which is the default) for XHR requests to get the `Referer` 41 header. Purely interactive Fossil features, in which users directly 42 activate links or forms, work with a level of 1 or higher. 43- **Chrome**: apparently requires an add-on in order to change this 44 policy, so Chrome without such an add-on will not suppress this 45 header. 46- **Safari**: ??? 47- **Other browsers**: ??? 48 49If `/fileedit` shows an error message saying "CSRF violation," the 50problem is that the browser is not sending a `Referer` header to XHR 51connections. Fossil does not offer a way to disable its CSRF 52protections. 53 54[referer]: https://en.wikipedia.org/wiki/HTTP_referer 55[csrf]: https://en.wikipedia.org/wiki/Cross-site_request_forgery 56[xhr]: https://en.wikipedia.org/wiki/XMLHttpRequest 57 58## <a id="commit"></a> `/fileedit` **Works by Creating Commits** 59 60Thus any edits made via that page become a normal part of the 61repository. 62 63## <a id="intent"></a> `/fileedit` is *Intended* for use with Embedded Docs 64 65... and similar text files, and is most certainly 66**not intended for editing code**. 67 68Editing files with unusual syntax requirements, e.g. hard tabs in 69makefiles, may break them. *You Have Been Warned.* 70 71Similarly, though every effort is made to retain the end-of-line 72style used by being-edited files, the round-trip through an HTML 73textarea element may change the EOLs. The Commit section of the page 74offers three different options for how to treat newlines when saving 75changes. **Files with mixed EOL styles** *will be normalized to a single 76EOL style* when modified using `/fileedit`. When "inheriting" the EOL 77style from a previous version which has mixed styles, the first EOL 78style detected in the previous version of the file is used. 79 80## <a id="checkout"></a> `/fileedit` **is Not a Replacement for a Checkout** 81 82A full-featured checkout allows far more possibilities than this basic 83online editor permits, and the feature scope of `/fileedit` is 84intentionally kept small, implementing only the bare necessities 85needed for performing basic edits online. It *is not, and will never 86be, a replacement for a checkout.* 87 88It is to be expected that users will want to do "more" with this 89page, and we generally encourage feature requests, but be aware that 90certain types of ostensibly sensible feature requests *will be 91rejected* for `/fileedit`. These include, but are not limited to: 92 93- Features which are already provided by other pages, e.g. 94the ability to create a new named branch or add tags. 95- Features which would require re-implementing significant 96capabilities provided only within a checkout (e.g. merging files). 97- The ability to edit/manipulate files which are in a local 98checkout. (If you have a checkout, use your local editor, not 99`/fileedit`.) 100- Editing of non-text files, e.g. images. Use a checkout and your 101preferred graphics editor. 102- Support for syncing/pulling/pushing of a repository before and/or 103after edits. Those features cannot be *reliably* provided via a web 104interface for several reasons. 105 106Similarly, some *potential* features have significant downsides, 107abuses, and/or implementation hurdles which make the decision of 108whether or not to implement them subject to notable contributor 109debate. e.g. the ability to add new files or remove/rename older 110files. 111 112 113## <a id="storage"></a> `/fileedit` **Stores Only Limited Local Edits While Working** 114 115When changes are made to a given checkin/file combination, 116`/fileedit` will, if possible, store them in [`window.localStorage` 117or `window.sessionStorage`][html5storage], if available, but... 118 119- Which storage is used is unspecified and may differ across 120 environments. 121- If neither of those is available, the storage is transient and 122 will not survive a page reload. In this case, the UI issues a clear 123 warning in the editor tab. 124- It stores only the most recent checkin/file combinations which have 125 been modified (exactly how many may differ - the number will be 126 noted somewhere in the UI). Note that changing the "executable bit" 127 is counted as a modification, but the checkin *comment* is *not* 128 and is reset after a commit. 129- If its internal limit on the number of modified files is exceeded, 130 it silently discards the oldest edits to keep the list at its limit. 131 132Edits are saved whenever the editor component fires its "change" 133event, which essentially means as soon as it loses input focus. Thus 134to force the browser to save any pending changes, simply click 135somwhere on the page outside of the editor. 136 137Exactly how long `localStorage` will survive, and how much it or 138`sessionStorage` can hold, is environment-dependent. `sessionStorage` 139will survive until the current browser tab is closed, but it survives 140across reloads of the same tab. 141 142If `/fileedit` determines that no persistent storage is available a 143warning is displayed on the editor page. 144 145[html5storage]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API 146 147## <a id="power"></a> The Power is Yours, but... 148 149> "With great power comes great responsibility." 150 151**Use this feature judiciously, *if at all*.** 152 153Now, with those warnings and caveats out of the way... 154 155----- 156 157# <a id="tips"></a> Tips and Tricks 158 159## <a id="global-js"></a> `fossil` Global-scope JS Object 160 161`/fileedit` is largely implemented in JavaScript, and makes heavy use 162of the global-scope `fossil` object, which provides 163infrastructure-level features intended for use by Fossil UI pages. 164(That said, that infrastructure was introduced with `/fileedit`, and 165most pages do not use it.) 166 167The `fossil.page` object represents the UI's current page (on pages 168which make use of this API - most do not). That object supports 169listening to page-specific events so that JS code installed via 170[client-side edits to the site skin's footer](customskin.md) may react 171to those changes somehow. The next section describes one such use for 172such events... 173 174## <a id="syn-hl"></a> Integrating Syntax Highlighting 175 176Assuming a repository has integrated a 3rd-party syntax highlighting 177solution, it can probably (depending on its API) be told how to 178highlight `/fileedit`'s wiki/markdown-format previews. Here are 179instructions for doing so with [highlightjs](https://highlightjs.org/): 180 181At the very bottom of the [site skin's footer](customskin.md), add a 182script tag similar to the following: 183 184```javascript 185<script nonce="$<nonce>"> 186if(window.fossil && fossil.page && fossil.page.name==='fileedit'){ 187 fossil.page.addEventListener( 188 'fileedit-preview-updated', 189 (ev)=>{ 190 if(ev.detail.previewMode==='wiki'){ 191 ev.detail.element.querySelectorAll( 192 'code[class^=language-]' 193 ).forEach((e)=>hljs.highlightBlock(e)); 194 } 195 } 196 ); 197} 198</script> 199``` 200 201Note that the `nonce="$<nonce>"` part is intended to be entered 202literally as shown above. It will be expanded to contain the current 203request's nonce value when the page is rendered. 204 205The first line of the script just ensures that the expected JS-level 206infrastructure is loaded. It's only loaded in the `/fileedit` page and 207possibly pages added or "upgraded" since `/fileedit`'s introduction. 208 209The part in the `if` block adds an event listener to the `/fileedit` 210app which gets called when the preview is refreshed. That event 211contains 3 properties: 212 213- `previewMode`: a string describing the current preview mode: `wiki` 214 (which includes Fossil-native wiki and markdown), `text`, 215 `htmlInline`, `htmlIframe`. We should "probably" only highlight wiki 216 text, and thus the example above limits its work to that type of 217 preview. It won't work with `htmlIframe`, as that represents an 218 iframe element which contains a complete HTML document. 219- `element`: the DOM element in which the preview is rendered. 220- `mimetype`: the mimetype of the being-previewed content, as determined 221 by Fossil (by its file extension). 222 223The event listener callback shown above doesn't use the `mimetype`, 224but makes used of the other two. It fishes all `code` blocks out of 225the preview which explicitly have a CSS class named 226`language-`something, and then asks highlightjs to highlight them. 227 228## <a id="editor"></a> Integrating a Custom Editor Widget 229 230(These instructions also work for the `/wikiedit` page by replacing 231"fileedit" with "wikiedit" in any strings or symbol names!) 232 233It is possible to replace `/fileedit`'s basic text-editing widget (a 234`textarea` element) with a fancy 3rd-party editor widget by following 235these instructions... 236 237All JavaScript code which follows is assumed to be in a script tag 238similar to the one shown in the previous section: 239 240```javascript 241<script nonce="$<nonce>"> 242if(window.fossil && fossil.page && fossil.page.name==='fileedit'){ 243 // code specific to the fileedit page goes here 244} 245</script> 246``` 247 248First, install proxy functions so that `fossil.page.fileContent()` 249can get and set your content: 250 251``` 252fossil.page.setContentMethods( 253 function(){ return text-form content of your widget }, 254 function(content){ set text-form content of your widget } 255}; 256``` 257 258Secondly, we need to alert the editor app when there are changes so 259that it can do things like store edits locally so that they are not 260lost on a page reload. How that is done is completely dependent on the 2613rd-party editor widget, but it generically looks something like: 262 263``` 264myCustomWidget.on('eventName', ()=>fossil.page.notifyOfChange()); 265``` 266 267(This feature requires fossil version 2.13 or later. In 2.12 it is 268possible to do this but requires making use of a "leaky abstraction".) 269 270Lastly, if the 3rd-party editor does *not* hide or remove the native 271editor widget, and does not inject itself into the DOM on the caller's 272behalf, we can replace the native widget with the 3rd-party one with: 273 274```javascript 275fossil.page.replaceEditorWidget(yourNewWidgetElement); 276``` 277 278That method must be passed a DOM element and may only be called once: 279it *removes itself* the first time it is called. 280 281That should be all there is to it. When `fossil.page` needs to get the 282being-edited content, it will call the installed content-getter 283function with no arguments, and when it sets the content (immediately 284after (re)loading a file or grabbing local edits), it will pass that 285content to the installed content-setter method. Those, in turn will 286trigger the installed proxies and fire any relevant events. 287 288Below is an example of Fossil skin footer content which plugs in the 289TinyMCE HTML editor into the `/wikiedit` page, but the process is 290identical for `/fileedit` (noting that `/fileedit` may need to be able 291to edit multiple types of files for which a special-purpose editor 292like TinyMCE may not be suitable). Note that any paths to CSS and JS 293resources of course need to be modified to suit one's own 294installation. 295 296``` 297<!-- TinyMCE CSS and JS: --> 298<link href="$<home>/doc/ckout/skin.min.css" rel="stylesheet" type="text/css"> 299<link href="$<home>/doc/ckout/content.min.css" rel="stylesheet" type="text/css"> 300<script src='$<home>/doc/ckout/tinymce.min.js'></script> 301<script src='$<home>/doc/ckout/theme.min.js'></script> 302<script src='$<home>/doc/ckout/icons.min.js'></script> 303<!-- Integrate TinyMCE into /wikiedit: --> 304<script nonce="$<nonce>"> 305if(window.fossil && window.fossil.page.name==='wikiedit'){ 306 window.fossil.onPageLoad( function(){ 307 const elemId = 'wikiedit-content-editor'; 308 tinymce.init({selector: 'textarea#'+elemId}); 309 const widget = tinymce.get(elemId); 310 fossil.page.setContentMethods( 311 function(){return widget.getContent()}, 312 function(content){widget.setContent(content)} 313 ); 314 widget.on('change', function(){ 315 if(widget.isDirty()) fossil.page.notifyOfChange(); 316 }); 317 }); 318} 319</script> 320``` 321