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