|
1
|
# The fileedit Page |
|
2
|
|
|
3
|
This document describes the limitations of, caveats for, and |
|
4
|
disclaimers for the [](/fileedit) page, which provides users with |
|
5
|
basic editing features for files via the web interface when they |
|
6
|
have [checkin privileges](./caps/index.md). |
|
7
|
|
|
8
|
# Important Caveats and Disclaimers |
|
9
|
|
|
10
|
Predictably, the ability to edit files in a repository from a web |
|
11
|
browser halfway around the world comes with several obligatory caveats |
|
12
|
and disclaimers... |
|
13
|
|
|
14
|
## <a id="cap"></a> `/fileedit` Does *Nothing* by Default. |
|
15
|
|
|
16
|
In order to "activate" it, a user with [the "setup" |
|
17
|
permission](./caps/index.md) must set the |
|
18
|
[fileedit-glob](/help/fileedit-glob) repository setting to a |
|
19
|
comma- or newline-delimited list of globs representing a whitelist of |
|
20
|
files which may be edited online. Any user with commit access may then |
|
21
|
edit files matching one of those globs. Certain pages within the UI |
|
22
|
get an "edit" link added to them when the current user's permissions |
|
23
|
and the whitelist both permit editing of that file. |
|
24
|
|
|
25
|
## <a id="csrf"></a> CSRF & HTTP Referrer Headers |
|
26
|
|
|
27
|
In order to protect against [Cross-site Request Forgery (CSRF)][csrf] |
|
28
|
attacks, Fossil UI features which write to the database require that |
|
29
|
the browser send the so-called [HTTP `Referer` header][referer] |
|
30
|
(noting that the misspelling of "referrer" is a historical accident |
|
31
|
which has long-since been standardized!). Modern browsers, by default, |
|
32
|
include such information automatically for *interactive* actions which |
|
33
|
lead to a request, e.g. clicking on a link back to the same |
|
34
|
server. However, `/fileedit` uses asynchronous ["XHR"][xhr] |
|
35
|
connections, which browsers *may* treat differently than strictly |
|
36
|
interactive 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
|
|
|
49
|
If `/fileedit` shows an error message saying "CSRF violation," the |
|
50
|
problem is that the browser is not sending a `Referer` header to XHR |
|
51
|
connections. Fossil does not offer a way to disable its CSRF |
|
52
|
protections. |
|
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
|
|
|
60
|
Thus any edits made via that page become a normal part of the |
|
61
|
repository. |
|
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
|
|
|
68
|
Editing files with unusual syntax requirements, e.g. hard tabs in |
|
69
|
makefiles, may break them. *You Have Been Warned.* |
|
70
|
|
|
71
|
Similarly, though every effort is made to retain the end-of-line |
|
72
|
style used by being-edited files, the round-trip through an HTML |
|
73
|
textarea element may change the EOLs. The Commit section of the page |
|
74
|
offers three different options for how to treat newlines when saving |
|
75
|
changes. **Files with mixed EOL styles** *will be normalized to a single |
|
76
|
EOL style* when modified using `/fileedit`. When "inheriting" the EOL |
|
77
|
style from a previous version which has mixed styles, the first EOL |
|
78
|
style 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
|
|
|
82
|
A full-featured checkout allows far more possibilities than this basic |
|
83
|
online editor permits, and the feature scope of `/fileedit` is |
|
84
|
intentionally kept small, implementing only the bare necessities |
|
85
|
needed for performing basic edits online. It *is not, and will never |
|
86
|
be, a replacement for a checkout.* |
|
87
|
|
|
88
|
It is to be expected that users will want to do "more" with this |
|
89
|
page, and we generally encourage feature requests, but be aware that |
|
90
|
certain types of ostensibly sensible feature requests *will be |
|
91
|
rejected* for `/fileedit`. These include, but are not limited to: |
|
92
|
|
|
93
|
- Features which are already provided by other pages, e.g. |
|
94
|
the ability to create a new named branch or add tags. |
|
95
|
- Features which would require re-implementing significant |
|
96
|
capabilities provided only within a checkout (e.g. merging files). |
|
97
|
- The ability to edit/manipulate files which are in a local |
|
98
|
checkout. (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 |
|
101
|
preferred graphics editor. |
|
102
|
- Support for syncing/pulling/pushing of a repository before and/or |
|
103
|
after edits. Those features cannot be *reliably* provided via a web |
|
104
|
interface for several reasons. |
|
105
|
|
|
106
|
Similarly, some *potential* features have significant downsides, |
|
107
|
abuses, and/or implementation hurdles which make the decision of |
|
108
|
whether or not to implement them subject to notable contributor |
|
109
|
debate. e.g. the ability to add new files or remove/rename older |
|
110
|
files. |
|
111
|
|
|
112
|
|
|
113
|
## <a id="storage"></a> `/fileedit` **Stores Only Limited Local Edits While Working** |
|
114
|
|
|
115
|
When changes are made to a given checkin/file combination, |
|
116
|
`/fileedit` will, if possible, store them in [`window.localStorage` |
|
117
|
or `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
|
|
|
132
|
Edits are saved whenever the editor component fires its "change" |
|
133
|
event, which essentially means as soon as it loses input focus. Thus |
|
134
|
to force the browser to save any pending changes, simply click |
|
135
|
somwhere on the page outside of the editor. |
|
136
|
|
|
137
|
Exactly how long `localStorage` will survive, and how much it or |
|
138
|
`sessionStorage` can hold, is environment-dependent. `sessionStorage` |
|
139
|
will survive until the current browser tab is closed, but it survives |
|
140
|
across reloads of the same tab. |
|
141
|
|
|
142
|
If `/fileedit` determines that no persistent storage is available a |
|
143
|
warning 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
|
|
|
153
|
Now, 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 |
|
162
|
of the global-scope `fossil` object, which provides |
|
163
|
infrastructure-level features intended for use by Fossil UI pages. |
|
164
|
(That said, that infrastructure was introduced with `/fileedit`, and |
|
165
|
most pages do not use it.) |
|
166
|
|
|
167
|
The `fossil.page` object represents the UI's current page (on pages |
|
168
|
which make use of this API - most do not). That object supports |
|
169
|
listening to page-specific events so that JS code installed via |
|
170
|
[client-side edits to the site skin's footer](customskin.md) may react |
|
171
|
to those changes somehow. The next section describes one such use for |
|
172
|
such events... |
|
173
|
|
|
174
|
## <a id="syn-hl"></a> Integrating Syntax Highlighting |
|
175
|
|
|
176
|
Assuming a repository has integrated a 3rd-party syntax highlighting |
|
177
|
solution, it can probably (depending on its API) be told how to |
|
178
|
highlight `/fileedit`'s wiki/markdown-format previews. Here are |
|
179
|
instructions for doing so with [highlightjs](https://highlightjs.org/): |
|
180
|
|
|
181
|
At the very bottom of the [site skin's footer](customskin.md), add a |
|
182
|
script tag similar to the following: |
|
183
|
|
|
184
|
```javascript |
|
185
|
<script nonce="$<nonce>"> |
|
186
|
if(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
|
|
|
201
|
Note that the `nonce="$<nonce>"` part is intended to be entered |
|
202
|
literally as shown above. It will be expanded to contain the current |
|
203
|
request's nonce value when the page is rendered. |
|
204
|
|
|
205
|
The first line of the script just ensures that the expected JS-level |
|
206
|
infrastructure is loaded. It's only loaded in the `/fileedit` page and |
|
207
|
possibly pages added or "upgraded" since `/fileedit`'s introduction. |
|
208
|
|
|
209
|
The part in the `if` block adds an event listener to the `/fileedit` |
|
210
|
app which gets called when the preview is refreshed. That event |
|
211
|
contains 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
|
|
|
223
|
The event listener callback shown above doesn't use the `mimetype`, |
|
224
|
but makes use of the other two. It fishes all `code` blocks out of |
|
225
|
the 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
|
|
|
233
|
It is possible to replace `/fileedit`'s basic text-editing widget (a |
|
234
|
`textarea` element) with a fancy 3rd-party editor widget by following |
|
235
|
these instructions... |
|
236
|
|
|
237
|
All JavaScript code which follows is assumed to be in a script tag |
|
238
|
similar to the one shown in the previous section: |
|
239
|
|
|
240
|
```javascript |
|
241
|
<script nonce="$<nonce>"> |
|
242
|
if(window.fossil && fossil.page && fossil.page.name==='fileedit'){ |
|
243
|
// code specific to the fileedit page goes here |
|
244
|
} |
|
245
|
</script> |
|
246
|
``` |
|
247
|
|
|
248
|
First, install proxy functions so that `fossil.page.fileContent()` |
|
249
|
can get and set your content: |
|
250
|
|
|
251
|
``` |
|
252
|
fossil.page.setContentMethods( |
|
253
|
function(){ return text-form content of your widget }, |
|
254
|
function(content){ set text-form content of your widget } |
|
255
|
}; |
|
256
|
``` |
|
257
|
|
|
258
|
Secondly, we need to alert the editor app when there are changes so |
|
259
|
that it can do things like store edits locally so that they are not |
|
260
|
lost on a page reload. How that is done is completely dependent on the |
|
261
|
3rd-party editor widget, but it generically looks something like: |
|
262
|
|
|
263
|
``` |
|
264
|
myCustomWidget.on('eventName', ()=>fossil.page.notifyOfChange()); |
|
265
|
``` |
|
266
|
|
|
267
|
Lastly, if the 3rd-party editor does *not* hide or remove the native |
|
268
|
editor widget, and does not inject itself into the DOM on the caller's |
|
269
|
behalf, we can replace the native widget with the 3rd-party one with: |
|
270
|
|
|
271
|
```javascript |
|
272
|
fossil.page.replaceEditorWidget(yourNewWidgetElement); |
|
273
|
``` |
|
274
|
|
|
275
|
That method must be passed a DOM element and may only be called once: |
|
276
|
it *removes itself* the first time it is called. |
|
277
|
|
|
278
|
That should be all there is to it. When `fossil.page` needs to get the |
|
279
|
being-edited content, it will call the installed content-getter |
|
280
|
function with no arguments, and when it sets the content (immediately |
|
281
|
after (re)loading a file or grabbing local edits), it will pass that |
|
282
|
content to the installed content-setter method. Those, in turn will |
|
283
|
trigger the installed proxies and fire any relevant events. |
|
284
|
|
|
285
|
Below is an example of Fossil skin footer content which plugs in the |
|
286
|
TinyMCE HTML editor into the `/wikiedit` page, but the process is |
|
287
|
identical for `/fileedit` (noting that `/fileedit` may need to be able |
|
288
|
to edit multiple types of files for which a special-purpose editor |
|
289
|
like TinyMCE may not be suitable). Note that any paths to CSS and JS |
|
290
|
resources of course need to be modified to suit one's own |
|
291
|
installation. |
|
292
|
|
|
293
|
``` |
|
294
|
<!-- TinyMCE CSS and JS: --> |
|
295
|
<link href="$<home>/doc/ckout/skin.min.css" rel="stylesheet" type="text/css"> |
|
296
|
<link href="$<home>/doc/ckout/content.min.css" rel="stylesheet" type="text/css"> |
|
297
|
<script src='$<home>/doc/ckout/tinymce.min.js'></script> |
|
298
|
<script src='$<home>/doc/ckout/theme.min.js'></script> |
|
299
|
<script src='$<home>/doc/ckout/icons.min.js'></script> |
|
300
|
<!-- Integrate TinyMCE into /wikiedit: --> |
|
301
|
<script nonce="$<nonce>"> |
|
302
|
if(window.fossil && window.fossil.page.name==='wikiedit'){ |
|
303
|
window.fossil.onPageLoad( function(){ |
|
304
|
const elemId = 'wikiedit-content-editor'; |
|
305
|
tinymce.init({selector: 'textarea#'+elemId}); |
|
306
|
const widget = tinymce.get(elemId); |
|
307
|
fossil.page.setContentMethods( |
|
308
|
function(){return widget.getContent()}, |
|
309
|
function(content){widget.setContent(content)} |
|
310
|
); |
|
311
|
widget.on('change', function(){ |
|
312
|
if(widget.isDirty()) fossil.page.notifyOfChange(); |
|
313
|
}); |
|
314
|
}); |
|
315
|
} |
|
316
|
</script> |
|
317
|
``` |
|
318
|
|