Fossil SCM

Implemented preview mode for plain text and wiki/embedded doc, but how to populate the necessary iframe for HTML preview, without a URL back to the preview content, eludes me.

stephan 2020-05-04 12:10 checkin-without-checkout
Commit 079030faaf762c218c7ffa04f8e617aaa07aebbce9c740483c0030fb0f48cf19
--- src/default_css.txt
+++ src/default_css.txt
@@ -862,10 +862,11 @@
862862
// max-width: 30em;
863863
// overflow: auto;
864864
// }
865865
// .fileedit-XXX => /fileedit page
866866
.fileedit-form textarea {
867
+ font-family: monospace;
867868
width: 100%;
868869
}
869870
.fileedit-form fieldset {
870871
border-radius: 0.5em;
871872
}
@@ -893,10 +894,17 @@
893894
code.fileedit-manifest {
894895
display: block;
895896
height: 16em;
896897
overflow: auto;
897898
}
899
+div.fileedit-preview {
900
+ margin: 0;
901
+ padding: 0;
902
+}
903
+.fileedit-preview > div:first-child {
904
+ border-bottom: 1px dashed;
905
+}
898906
.input-with-label {
899907
border: 1px inset #808080;
900908
border-radius: 0.5em;
901909
padding: 0.25em 0.4em;
902910
margin: 0 0.5em;
903911
--- src/default_css.txt
+++ src/default_css.txt
@@ -862,10 +862,11 @@
862 // max-width: 30em;
863 // overflow: auto;
864 // }
865 // .fileedit-XXX => /fileedit page
866 .fileedit-form textarea {
 
867 width: 100%;
868 }
869 .fileedit-form fieldset {
870 border-radius: 0.5em;
871 }
@@ -893,10 +894,17 @@
893 code.fileedit-manifest {
894 display: block;
895 height: 16em;
896 overflow: auto;
897 }
 
 
 
 
 
 
 
898 .input-with-label {
899 border: 1px inset #808080;
900 border-radius: 0.5em;
901 padding: 0.25em 0.4em;
902 margin: 0 0.5em;
903
--- src/default_css.txt
+++ src/default_css.txt
@@ -862,10 +862,11 @@
862 // max-width: 30em;
863 // overflow: auto;
864 // }
865 // .fileedit-XXX => /fileedit page
866 .fileedit-form textarea {
867 font-family: monospace;
868 width: 100%;
869 }
870 .fileedit-form fieldset {
871 border-radius: 0.5em;
872 }
@@ -893,10 +894,17 @@
894 code.fileedit-manifest {
895 display: block;
896 height: 16em;
897 overflow: auto;
898 }
899 div.fileedit-preview {
900 margin: 0;
901 padding: 0;
902 }
903 .fileedit-preview > div:first-child {
904 border-bottom: 1px dashed;
905 }
906 .input-with-label {
907 border: 1px inset #808080;
908 border-radius: 0.5em;
909 padding: 0.25em 0.4em;
910 margin: 0 0.5em;
911
+191 -35
--- src/fileedit.c
+++ src/fileedit.c
@@ -905,15 +905,42 @@
905905
}else{
906906
CX("</script>\n");
907907
}
908908
}
909909
910
-#if 0
911910
/*
912
-** This function is for potential TODO features for /fileedit.
913
-** It's been tested with that code but is not currently used
914
-** by it.
911
+** Emits a script tag which defines window.fossilFetch(), which works
912
+** similarly (not identically) to the not-quite-ubiquitous global
913
+** fetch().
914
+**
915
+** JS usages:
916
+**
917
+** fossilFetch( URI, onLoadCallback );
918
+**
919
+** fossilFetch( URI, optionsObject );
920
+**
921
+** Where the optionsObject may be an object with any of these
922
+** properties:
923
+**
924
+** - onload: callback(responseData) (default = output response to
925
+** console).
926
+**
927
+** - onerror: callback(XHR onload event) (default = no-op)
928
+**
929
+** - method: 'POST' | 'GET' (default = 'GET')
930
+**
931
+** Noting that URI must be relative to the top of the repository and
932
+** must not start with a slash. It gets %R/ prepended to it.
933
+**
934
+** TODOs, if needed, include:
935
+**
936
+** optionsObject.params: object map of key/value pairs to append to the
937
+** URI.
938
+**
939
+** optionsObject.payload: string or JSON-able object to POST as the
940
+** payload.
941
+**
915942
*/
916943
static void fileedit_emit_script_fetch(){
917944
fileedit_emit_script(0);
918945
CX("window.fossilFetch = function(path,opt){\n");
919946
CX(" if('function'===typeof opt){\n");
@@ -935,11 +962,10 @@
935962
CX(" }\n");
936963
CX(" x.send();");
937964
CX("};\n");
938965
fileedit_emit_script(1);
939966
};
940
-#endif /* fileedit_emit_script_fetch() */
941967
942968
/*
943969
** Outputs a labeled checkbox element:
944970
**
945971
** <span class='input-with-label' title={{zTip}}>
@@ -962,10 +988,84 @@
962988
CX("><input type='checkbox' name='%s' value='%T'%s/>",
963989
zFieldName,
964990
zValue ? zValue : "", isChecked ? " checked" : "");
965991
CX("<span>%h</span></span>", zLabel);
966992
}
993
+
994
+enum fileedit_render_preview_flags {
995
+FE_PREVIEW_LINE_NUMBERS = 1
996
+};
997
+
998
+/*
999
+** Performs the PREVIEW mode for /filepage.
1000
+*/
1001
+static void fileedit_render_preview(Blob * pContent,
1002
+ const char *zFilename,
1003
+ int flags){
1004
+ const char * zMime;
1005
+ enum render_modes {PLAIN_TEXT = 0, HTML, WIKI};
1006
+ int renderMode = PLAIN_TEXT;
1007
+ zMime = mimetype_from_name(zFilename);
1008
+ if( zMime ){
1009
+ if( fossil_strcmp(zMime, "text/html")==0 ){
1010
+ renderMode = HTML;
1011
+ }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
1012
+ || fossil_strcmp(zMime, "text/x-markdown")==0 ){
1013
+ renderMode = WIKI;
1014
+ }
1015
+ }
1016
+ CX("<div class='fileedit-preview'>");
1017
+ CX("<div>Preview</div>");
1018
+ switch(renderMode){
1019
+ case HTML:{
1020
+ CX("<iframe width='100%%' frameborder='0' marginwidth='0' "
1021
+ "marginheight='0' sandbox='allow-same-origin' id='ifm1' "
1022
+ "srcdoc='Not yet working: not sure how to "
1023
+ "populate the iframe.'"
1024
+ "></iframe>");
1025
+#if 0
1026
+ fileedit_emit_script(0);
1027
+ CX("document.getElementById('ifm1').addEventListener('load',"
1028
+ "function(){\n"
1029
+ "console.debug('iframe=',this);\n"
1030
+ "this.height=this.contentDocument.documentElement."
1031
+ "scrollHeight + 75;\n"
1032
+ "this.contentDocument.body.innerHTML=`%h`;\n"
1033
+ "});\n",
1034
+ blob_str(pContent));
1035
+ /* Potential TODO: use iframe.srcdoc:
1036
+ **
1037
+ ** https://caniuse.com/#search=srcdoc
1038
+ ** https://stackoverflow.com/questions/22381216/escape-quotes-in-an-iframe-srcdoc-value
1039
+ **
1040
+ ** Doing so would require escaping the quote characters which match
1041
+ ** the srcdoc='xyz' quotes.
1042
+ */
1043
+ fileedit_emit_script(1);
1044
+#endif
1045
+ break;
1046
+ }
1047
+ case WIKI:
1048
+ wiki_render_by_mimetype(pContent, zMime);
1049
+ break;
1050
+ case PLAIN_TEXT:
1051
+ default:{
1052
+ const char *zExt = strrchr(zFilename,'.');
1053
+ const char *zContent = blob_str(pContent);
1054
+ if(FE_PREVIEW_LINE_NUMBERS & flags){
1055
+ output_text_with_line_numbers(zContent, "on");
1056
+ }else if(zExt && zExt[1]){
1057
+ CX("<pre><code class='language-%s'>%h</code></pre>",
1058
+ zExt+1, zContent);
1059
+ }else{
1060
+ CX("<pre>%h</pre>", zExt+1, zContent);
1061
+ }
1062
+ break;
1063
+ }
1064
+ }
1065
+ CX("</div><!--.fileedit-preview-->\n");
1066
+}
9671067
9681068
/*
9691069
** WEBPAGE: fileedit
9701070
**
9711071
** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -980,30 +1080,55 @@
9801080
** All other parameters are for internal use only, submitted via the
9811081
** form-submission process, and may change with any given revision of
9821082
** this code.
9831083
*/
9841084
void fileedit_page(){
985
- const char * zFilename = PD("file",P("name")); /* filename */
1085
+ const char * zFilename = PD("file",P("name"));
1086
+ /* filename. We'll accept 'name'
1087
+ because that param is handled
1088
+ specially by the core. */
9861089
const char * zRev = P("r"); /* checkin version */
9871090
const char * zContent = P("content"); /* file content */
9881091
const char * zComment = P("comment"); /* checkin comment */
9891092
CheckinMiniInfo cimi; /* Checkin state */
9901093
int submitMode = 0; /* See mapping below */
9911094
int vid, newVid = 0; /* checkin rid */
992
- char * zFileUuid = 0; /* File content UUID */
9931095
int frid = 0; /* File content rid */
1096
+ int previewLn = P("preview_ln")!=0; /* Line number mode */
1097
+ char * zFileUuid = 0; /* File content UUID */
9941098
Blob err = empty_blob; /* Error report */
9951099
const char * zFlagCheck = 0; /* Temp url flag holder */
9961100
Blob endScript = empty_blob; /* Script code to run at the
9971101
end. This content will be
9981102
combined into a single JS
9991103
function call, thus each
10001104
entry must end with a
10011105
semicolon. */
10021106
Stmt stmt = empty_Stmt;
1107
+ const int loadMode = 0; /* See next comment block */
1108
+ /* loadMode: How to populate the TEXTAREA:
1109
+ **
1110
+ ** 0: HTML encode: despite my personal reservations regarding HTML
1111
+ ** escaping, this seems to be the only reliable approach
1112
+ ** until/unless we completely AJAXify this page.
1113
+ **
1114
+ ** 1: JSON mode: JSON-izes the file content and injects it, via JS,
1115
+ ** into the editor TEXTAREA. This works wonderfully until the input
1116
+ ** file contains an raw <SCRIPT> tag, at which points the HTML
1117
+ ** parser chokes on it.
1118
+ **
1119
+ ** 2: AJAX mode: can only load content from the db, not preview/dry-run
1120
+ ** content. Unless this page is refactored to work solely over AJAX
1121
+ ** (which is a potential TODO), this method is only useful on the
1122
+ ** initial hit to this page, where the file is loaded.
1123
+ **
1124
+ ** loadMode is not generally configurable: change it only for
1125
+ ** testing/development purposes.
1126
+ */
10031127
#define fail(EXPR) blob_appendf EXPR; goto end_footer
10041128
1129
+ assert(loadMode==0 || loadMode==1 || loadMode==2);
10051130
login_check_credentials();
10061131
if( !g.perm.Write ){
10071132
login_needed(g.anon.Write);
10081133
return;
10091134
}
@@ -1023,11 +1148,12 @@
10231148
/* As of this point, don't use return or fossil_fatal(), use
10241149
** fail((&err,...)) instead so that we can be sure to do any
10251150
** cleanup and end the transaction cleanly.
10261151
*/
10271152
if(!zRev || !*zRev || !zFilename || !*zFilename){
1028
- fail((&err,"Missing required URL parameters."));
1153
+ fail((&err,"Missing required URL parameters: "
1154
+ "file=FILE and r=CHECKIN"));
10291155
}
10301156
if(0==fileedit_is_editable(zFilename)){
10311157
fail((&err,"Filename <code>%h</code> is disallowed "
10321158
"by the <code>fileedit-glob</code> repository "
10331159
"setting.",
@@ -1035,10 +1161,11 @@
10351161
}
10361162
vid = symbolic_name_to_rid(zRev, "ci");
10371163
if(0==vid){
10381164
fail((&err,"Could not resolve checkin version."));
10391165
}
1166
+ cimi.zFilename = mprintf("%s",zFilename);
10401167
10411168
/* Find the repo-side file entry or fail... */
10421169
cimi.zParentUuid = rid_to_uuid(vid);
10431170
db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
10441171
"WHERE filename=%Q %s AND checkinID=%d",
@@ -1075,13 +1202,18 @@
10751202
10761203
/* All set. Here we go... */
10771204
10781205
CX("<h1>Editing:</h1>");
10791206
CX("<p class='fileedit-hint'>");
1080
- CX("File: <code>%h</code><br>"
1081
- "Checkin Version: <code id='r-label'>%s</code><br>",
1082
- zFilename, cimi.zParentUuid);
1207
+ CX("File: "
1208
+ "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
1209
+ "<code>%h</code><br>",
1210
+ zFilename, zFileUuid, zFilename);
1211
+ CX("Checkin Version: "
1212
+ "[<a id='r-link' href='%R/info/%!S'>info</a>] "
1213
+ "<code id='r-label'>%s</code><br>",
1214
+ cimi.zParentUuid, cimi.zParentUuid);
10831215
CX("Permalink: <code>"
10841216
"<a id='permalink' href='%R/fileedit?file=%T&r=%!S'>"
10851217
"/fileedit?file=%T&r=%!S</a></code><br>"
10861218
"(Clicking the permalink will reload the page and discard "
10871219
"all edits!)",
@@ -1090,12 +1222,13 @@
10901222
CX("</p>");
10911223
CX("<p>This page is <em>far from complete</em> and may still have "
10921224
"significant bugs. USE AT YOUR OWN RISK, preferably on a test "
10931225
"repo.</p>\n");
10941226
1095
- CX("<form action='%R/fileedit' method='POST' "
1096
- "class='fileedit-form'>\n");
1227
+ CX("<form action='%R/fileedit%s' method='POST' "
1228
+ "class='fileedit-form'>\n",
1229
+ submitMode>0 ? "#options" : "");
10971230
10981231
/******* Hidden fields *******/
10991232
CX("<input type='hidden' name='r' value='%s'>",
11001233
cimi.zParentUuid);
11011234
CX("<input type='hidden' name='file' value='%T'>",
@@ -1113,14 +1246,19 @@
11131246
11141247
/******* Content *******/
11151248
CX("<h3>File Content</h3>\n");
11161249
CX("<textarea name='content' id='fileedit-content' "
11171250
"rows='20' cols='80'>");
1118
- CX("Loading...");
1251
+ if(0==loadMode){
1252
+ CX("%h",blob_str(&cimi.fileContent));
1253
+ }else{
1254
+ CX("Loading...");
1255
+ /* Performed via JS later on */
1256
+ }
11191257
CX("</textarea>\n");
11201258
/******* Flags/options *******/
1121
- CX("<fieldset class='fileedit-options'>"
1259
+ CX("<fieldset class='fileedit-options' id='options'>"
11221260
"<legend>Options</legend><div>"
11231261
/* Chrome does not sanely lay out multiple
11241262
** fieldset children after the <legend>, so
11251263
** a containing div is necessary. */);
11261264
/*
@@ -1192,36 +1330,33 @@
11921330
CX("</select>");
11931331
}
11941332
11951333
CX("</div></fieldset>") /* end of checkboxes */;
11961334
1197
- /******* Buttons *******/
1335
+ /******* Buttons *******/
1336
+ CX("<a id='buttons'></a>");
11981337
CX("<fieldset class='fileedit-options'>"
11991338
"<legend>Tell the server to...</legend><div>");
12001339
CX("<button type='submit' name='submit' value='1'>"
12011340
"Save</button>");
12021341
CX("<button type='submit' name='submit' value='2'>"
1203
- "Preview (TODO)</button>");
1342
+ "Preview</button>");
12041343
CX("<button type='submit' name='submit' value='3'>"
12051344
"Diff (TODO)</button>");
1345
+ CX("<br>");
1346
+ style_labeled_checkbox("preview_ln",
1347
+ "Add line numbers to plain-text previews?", "1",
1348
+ "If on, plain-text files (only) will get "
1349
+ "line numbers added to the preview.",
1350
+ previewLn);
12061351
CX("</div></fieldset>");
12071352
12081353
/******* End of form *******/
12091354
CX("</form>\n");
12101355
1211
- {
1212
- /* Populate the editor...
1213
- **
1214
- ** To avoid all escaping-related issues, we have to do this one of
1215
- ** two ways:
1216
- **
1217
- ** 1) Fetch the content via AJAX. That only works if the content
1218
- ** is already in the db, but not for edited versions.
1219
- **
1220
- ** 2) Store the content as JSON and feed it into the textarea
1221
- ** using JavaScript.
1222
- */
1356
+ /* Dynamically populate the editor... */
1357
+ if(1==loadMode || (2==loadMode && submitMode>0)){
12231358
char const * zQuoted = 0;
12241359
if(blob_size(&cimi.fileContent)>0){
12251360
db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
12261361
db_step(&stmt);
12271362
zQuoted = db_column_text(&stmt,0);
@@ -1231,10 +1366,21 @@
12311366
"document.getElementById('fileedit-content')"
12321367
".value=%s;", zQuoted ? zQuoted : "'';\n");
12331368
if(stmt.pStmt){
12341369
db_finalize(&stmt);
12351370
}
1371
+ }else if(2==loadMode){
1372
+ assert(submitMode==0);
1373
+ fileedit_emit_script_fetch();
1374
+ blob_appendf(&endScript,
1375
+ "window.fossilFetch('raw/%s',{"
1376
+ "onload: (r)=>document.getElementById('fileedit-content')"
1377
+ ".value=r,"
1378
+ "onerror:()=>document.getElementById('fileedit-content')"
1379
+ ".value="
1380
+ "'Error loading content'"
1381
+ "});\n", zFileUuid);
12361382
}
12371383
12381384
if(1==submitMode/*save*/){
12391385
Blob manifest = empty_blob;
12401386
char * zNewUuid = 0;
@@ -1244,11 +1390,10 @@
12441390
}else{
12451391
fail((&err,"Empty comment is not permitted."));
12461392
}
12471393
/*cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
12481394
assert(cimi.pParent && "We know vid is valid.");*/
1249
- cimi.zFilename = mprintf("%s",zFilename);
12501395
cimi.pMfOut = &manifest;
12511396
checkin_mini(&cimi, &newVid, &err);
12521397
if(newVid!=0){
12531398
zNewUuid = rid_to_uuid(newVid);
12541399
CX("<h3>Manifest%s: %S</h3><pre>"
@@ -1269,31 +1414,42 @@
12691414
blob_appendf(&endScript,
12701415
"/* Update version number */\n"
12711416
"document.querySelector('input[name=r]')"
12721417
".value=%Q;\n"
12731418
"document.querySelector('#r-label')"
1274
- ".innerText=%Q;\n",
1275
- zNewUuid, zNewUuid);
1419
+ ".innerText=%Q;\n"
1420
+ "document.querySelector('#r-link')"
1421
+ ".setAttribute('href', '%R/info/%!S');\n"
1422
+ "document.querySelector('#finfo-link')"
1423
+ ".setAttribute('href','%R/finfo?name=%T&m=%!S');\n",
1424
+ /*input[name=r]:*/zNewUuid, /*#r-label:*/ zNewUuid,
1425
+ /*#r-link:*/ zNewUuid,
1426
+ /*#finfo-link:*/zFilename, zNewUuid);
1427
+ blob_appendf(&endScript,
1428
+ "/* Updated finfo link */"
1429
+ );
12761430
blob_appendf(&endScript,
12771431
"/* Update permalink */\n"
12781432
"const urlFull='%R/fileedit?file=%T&r=%!S';\n"
12791433
"const urlShort='/fileedit?file=%T&r=%!S';\n"
12801434
"let link=document.querySelector('#permalink');\n"
12811435
"link.innerText=urlShort;\n"
12821436
"link.setAttribute('href',urlFull);\n",
1283
- zFilename, zNewUuid, zFilename, zNewUuid);
1437
+ cimi.zFilename, zNewUuid,
1438
+ cimi.zFilename, zNewUuid);
12841439
}
12851440
fossil_free(zNewUuid);
12861441
zNewUuid = 0;
12871442
}
12881443
/* On error, the error message is in the err blob and will
12891444
** be emitted below. */
12901445
cimi.pMfOut = 0;
12911446
blob_reset(&manifest);
12921447
}else if(2==submitMode/*preview*/){
1293
- /* TODO */
1294
- fail((&err,"Preview mode is still TODO."));
1448
+ int pflags = 0;
1449
+ if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1450
+ fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags);
12951451
}else if(3==submitMode/*diff*/){
12961452
fail((&err,"Diff mode is still TODO."));
12971453
}else{
12981454
/* Ignore invalid submitMode value */
12991455
goto end_footer;
13001456
--- src/fileedit.c
+++ src/fileedit.c
@@ -905,15 +905,42 @@
905 }else{
906 CX("</script>\n");
907 }
908 }
909
910 #if 0
911 /*
912 ** This function is for potential TODO features for /fileedit.
913 ** It's been tested with that code but is not currently used
914 ** by it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
915 */
916 static void fileedit_emit_script_fetch(){
917 fileedit_emit_script(0);
918 CX("window.fossilFetch = function(path,opt){\n");
919 CX(" if('function'===typeof opt){\n");
@@ -935,11 +962,10 @@
935 CX(" }\n");
936 CX(" x.send();");
937 CX("};\n");
938 fileedit_emit_script(1);
939 };
940 #endif /* fileedit_emit_script_fetch() */
941
942 /*
943 ** Outputs a labeled checkbox element:
944 **
945 ** <span class='input-with-label' title={{zTip}}>
@@ -962,10 +988,84 @@
962 CX("><input type='checkbox' name='%s' value='%T'%s/>",
963 zFieldName,
964 zValue ? zValue : "", isChecked ? " checked" : "");
965 CX("<span>%h</span></span>", zLabel);
966 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
968 /*
969 ** WEBPAGE: fileedit
970 **
971 ** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -980,30 +1080,55 @@
980 ** All other parameters are for internal use only, submitted via the
981 ** form-submission process, and may change with any given revision of
982 ** this code.
983 */
984 void fileedit_page(){
985 const char * zFilename = PD("file",P("name")); /* filename */
 
 
 
986 const char * zRev = P("r"); /* checkin version */
987 const char * zContent = P("content"); /* file content */
988 const char * zComment = P("comment"); /* checkin comment */
989 CheckinMiniInfo cimi; /* Checkin state */
990 int submitMode = 0; /* See mapping below */
991 int vid, newVid = 0; /* checkin rid */
992 char * zFileUuid = 0; /* File content UUID */
993 int frid = 0; /* File content rid */
 
 
994 Blob err = empty_blob; /* Error report */
995 const char * zFlagCheck = 0; /* Temp url flag holder */
996 Blob endScript = empty_blob; /* Script code to run at the
997 end. This content will be
998 combined into a single JS
999 function call, thus each
1000 entry must end with a
1001 semicolon. */
1002 Stmt stmt = empty_Stmt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003 #define fail(EXPR) blob_appendf EXPR; goto end_footer
1004
 
1005 login_check_credentials();
1006 if( !g.perm.Write ){
1007 login_needed(g.anon.Write);
1008 return;
1009 }
@@ -1023,11 +1148,12 @@
1023 /* As of this point, don't use return or fossil_fatal(), use
1024 ** fail((&err,...)) instead so that we can be sure to do any
1025 ** cleanup and end the transaction cleanly.
1026 */
1027 if(!zRev || !*zRev || !zFilename || !*zFilename){
1028 fail((&err,"Missing required URL parameters."));
 
1029 }
1030 if(0==fileedit_is_editable(zFilename)){
1031 fail((&err,"Filename <code>%h</code> is disallowed "
1032 "by the <code>fileedit-glob</code> repository "
1033 "setting.",
@@ -1035,10 +1161,11 @@
1035 }
1036 vid = symbolic_name_to_rid(zRev, "ci");
1037 if(0==vid){
1038 fail((&err,"Could not resolve checkin version."));
1039 }
 
1040
1041 /* Find the repo-side file entry or fail... */
1042 cimi.zParentUuid = rid_to_uuid(vid);
1043 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1044 "WHERE filename=%Q %s AND checkinID=%d",
@@ -1075,13 +1202,18 @@
1075
1076 /* All set. Here we go... */
1077
1078 CX("<h1>Editing:</h1>");
1079 CX("<p class='fileedit-hint'>");
1080 CX("File: <code>%h</code><br>"
1081 "Checkin Version: <code id='r-label'>%s</code><br>",
1082 zFilename, cimi.zParentUuid);
 
 
 
 
 
1083 CX("Permalink: <code>"
1084 "<a id='permalink' href='%R/fileedit?file=%T&r=%!S'>"
1085 "/fileedit?file=%T&r=%!S</a></code><br>"
1086 "(Clicking the permalink will reload the page and discard "
1087 "all edits!)",
@@ -1090,12 +1222,13 @@
1090 CX("</p>");
1091 CX("<p>This page is <em>far from complete</em> and may still have "
1092 "significant bugs. USE AT YOUR OWN RISK, preferably on a test "
1093 "repo.</p>\n");
1094
1095 CX("<form action='%R/fileedit' method='POST' "
1096 "class='fileedit-form'>\n");
 
1097
1098 /******* Hidden fields *******/
1099 CX("<input type='hidden' name='r' value='%s'>",
1100 cimi.zParentUuid);
1101 CX("<input type='hidden' name='file' value='%T'>",
@@ -1113,14 +1246,19 @@
1113
1114 /******* Content *******/
1115 CX("<h3>File Content</h3>\n");
1116 CX("<textarea name='content' id='fileedit-content' "
1117 "rows='20' cols='80'>");
1118 CX("Loading...");
 
 
 
 
 
1119 CX("</textarea>\n");
1120 /******* Flags/options *******/
1121 CX("<fieldset class='fileedit-options'>"
1122 "<legend>Options</legend><div>"
1123 /* Chrome does not sanely lay out multiple
1124 ** fieldset children after the <legend>, so
1125 ** a containing div is necessary. */);
1126 /*
@@ -1192,36 +1330,33 @@
1192 CX("</select>");
1193 }
1194
1195 CX("</div></fieldset>") /* end of checkboxes */;
1196
1197 /******* Buttons *******/
 
1198 CX("<fieldset class='fileedit-options'>"
1199 "<legend>Tell the server to...</legend><div>");
1200 CX("<button type='submit' name='submit' value='1'>"
1201 "Save</button>");
1202 CX("<button type='submit' name='submit' value='2'>"
1203 "Preview (TODO)</button>");
1204 CX("<button type='submit' name='submit' value='3'>"
1205 "Diff (TODO)</button>");
 
 
 
 
 
 
1206 CX("</div></fieldset>");
1207
1208 /******* End of form *******/
1209 CX("</form>\n");
1210
1211 {
1212 /* Populate the editor...
1213 **
1214 ** To avoid all escaping-related issues, we have to do this one of
1215 ** two ways:
1216 **
1217 ** 1) Fetch the content via AJAX. That only works if the content
1218 ** is already in the db, but not for edited versions.
1219 **
1220 ** 2) Store the content as JSON and feed it into the textarea
1221 ** using JavaScript.
1222 */
1223 char const * zQuoted = 0;
1224 if(blob_size(&cimi.fileContent)>0){
1225 db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1226 db_step(&stmt);
1227 zQuoted = db_column_text(&stmt,0);
@@ -1231,10 +1366,21 @@
1231 "document.getElementById('fileedit-content')"
1232 ".value=%s;", zQuoted ? zQuoted : "'';\n");
1233 if(stmt.pStmt){
1234 db_finalize(&stmt);
1235 }
 
 
 
 
 
 
 
 
 
 
 
1236 }
1237
1238 if(1==submitMode/*save*/){
1239 Blob manifest = empty_blob;
1240 char * zNewUuid = 0;
@@ -1244,11 +1390,10 @@
1244 }else{
1245 fail((&err,"Empty comment is not permitted."));
1246 }
1247 /*cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
1248 assert(cimi.pParent && "We know vid is valid.");*/
1249 cimi.zFilename = mprintf("%s",zFilename);
1250 cimi.pMfOut = &manifest;
1251 checkin_mini(&cimi, &newVid, &err);
1252 if(newVid!=0){
1253 zNewUuid = rid_to_uuid(newVid);
1254 CX("<h3>Manifest%s: %S</h3><pre>"
@@ -1269,31 +1414,42 @@
1269 blob_appendf(&endScript,
1270 "/* Update version number */\n"
1271 "document.querySelector('input[name=r]')"
1272 ".value=%Q;\n"
1273 "document.querySelector('#r-label')"
1274 ".innerText=%Q;\n",
1275 zNewUuid, zNewUuid);
 
 
 
 
 
 
 
 
 
1276 blob_appendf(&endScript,
1277 "/* Update permalink */\n"
1278 "const urlFull='%R/fileedit?file=%T&r=%!S';\n"
1279 "const urlShort='/fileedit?file=%T&r=%!S';\n"
1280 "let link=document.querySelector('#permalink');\n"
1281 "link.innerText=urlShort;\n"
1282 "link.setAttribute('href',urlFull);\n",
1283 zFilename, zNewUuid, zFilename, zNewUuid);
 
1284 }
1285 fossil_free(zNewUuid);
1286 zNewUuid = 0;
1287 }
1288 /* On error, the error message is in the err blob and will
1289 ** be emitted below. */
1290 cimi.pMfOut = 0;
1291 blob_reset(&manifest);
1292 }else if(2==submitMode/*preview*/){
1293 /* TODO */
1294 fail((&err,"Preview mode is still TODO."));
 
1295 }else if(3==submitMode/*diff*/){
1296 fail((&err,"Diff mode is still TODO."));
1297 }else{
1298 /* Ignore invalid submitMode value */
1299 goto end_footer;
1300
--- src/fileedit.c
+++ src/fileedit.c
@@ -905,15 +905,42 @@
905 }else{
906 CX("</script>\n");
907 }
908 }
909
 
910 /*
911 ** Emits a script tag which defines window.fossilFetch(), which works
912 ** similarly (not identically) to the not-quite-ubiquitous global
913 ** fetch().
914 **
915 ** JS usages:
916 **
917 ** fossilFetch( URI, onLoadCallback );
918 **
919 ** fossilFetch( URI, optionsObject );
920 **
921 ** Where the optionsObject may be an object with any of these
922 ** properties:
923 **
924 ** - onload: callback(responseData) (default = output response to
925 ** console).
926 **
927 ** - onerror: callback(XHR onload event) (default = no-op)
928 **
929 ** - method: 'POST' | 'GET' (default = 'GET')
930 **
931 ** Noting that URI must be relative to the top of the repository and
932 ** must not start with a slash. It gets %R/ prepended to it.
933 **
934 ** TODOs, if needed, include:
935 **
936 ** optionsObject.params: object map of key/value pairs to append to the
937 ** URI.
938 **
939 ** optionsObject.payload: string or JSON-able object to POST as the
940 ** payload.
941 **
942 */
943 static void fileedit_emit_script_fetch(){
944 fileedit_emit_script(0);
945 CX("window.fossilFetch = function(path,opt){\n");
946 CX(" if('function'===typeof opt){\n");
@@ -935,11 +962,10 @@
962 CX(" }\n");
963 CX(" x.send();");
964 CX("};\n");
965 fileedit_emit_script(1);
966 };
 
967
968 /*
969 ** Outputs a labeled checkbox element:
970 **
971 ** <span class='input-with-label' title={{zTip}}>
@@ -962,10 +988,84 @@
988 CX("><input type='checkbox' name='%s' value='%T'%s/>",
989 zFieldName,
990 zValue ? zValue : "", isChecked ? " checked" : "");
991 CX("<span>%h</span></span>", zLabel);
992 }
993
994 enum fileedit_render_preview_flags {
995 FE_PREVIEW_LINE_NUMBERS = 1
996 };
997
998 /*
999 ** Performs the PREVIEW mode for /filepage.
1000 */
1001 static void fileedit_render_preview(Blob * pContent,
1002 const char *zFilename,
1003 int flags){
1004 const char * zMime;
1005 enum render_modes {PLAIN_TEXT = 0, HTML, WIKI};
1006 int renderMode = PLAIN_TEXT;
1007 zMime = mimetype_from_name(zFilename);
1008 if( zMime ){
1009 if( fossil_strcmp(zMime, "text/html")==0 ){
1010 renderMode = HTML;
1011 }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
1012 || fossil_strcmp(zMime, "text/x-markdown")==0 ){
1013 renderMode = WIKI;
1014 }
1015 }
1016 CX("<div class='fileedit-preview'>");
1017 CX("<div>Preview</div>");
1018 switch(renderMode){
1019 case HTML:{
1020 CX("<iframe width='100%%' frameborder='0' marginwidth='0' "
1021 "marginheight='0' sandbox='allow-same-origin' id='ifm1' "
1022 "srcdoc='Not yet working: not sure how to "
1023 "populate the iframe.'"
1024 "></iframe>");
1025 #if 0
1026 fileedit_emit_script(0);
1027 CX("document.getElementById('ifm1').addEventListener('load',"
1028 "function(){\n"
1029 "console.debug('iframe=',this);\n"
1030 "this.height=this.contentDocument.documentElement."
1031 "scrollHeight + 75;\n"
1032 "this.contentDocument.body.innerHTML=`%h`;\n"
1033 "});\n",
1034 blob_str(pContent));
1035 /* Potential TODO: use iframe.srcdoc:
1036 **
1037 ** https://caniuse.com/#search=srcdoc
1038 ** https://stackoverflow.com/questions/22381216/escape-quotes-in-an-iframe-srcdoc-value
1039 **
1040 ** Doing so would require escaping the quote characters which match
1041 ** the srcdoc='xyz' quotes.
1042 */
1043 fileedit_emit_script(1);
1044 #endif
1045 break;
1046 }
1047 case WIKI:
1048 wiki_render_by_mimetype(pContent, zMime);
1049 break;
1050 case PLAIN_TEXT:
1051 default:{
1052 const char *zExt = strrchr(zFilename,'.');
1053 const char *zContent = blob_str(pContent);
1054 if(FE_PREVIEW_LINE_NUMBERS & flags){
1055 output_text_with_line_numbers(zContent, "on");
1056 }else if(zExt && zExt[1]){
1057 CX("<pre><code class='language-%s'>%h</code></pre>",
1058 zExt+1, zContent);
1059 }else{
1060 CX("<pre>%h</pre>", zExt+1, zContent);
1061 }
1062 break;
1063 }
1064 }
1065 CX("</div><!--.fileedit-preview-->\n");
1066 }
1067
1068 /*
1069 ** WEBPAGE: fileedit
1070 **
1071 ** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -980,30 +1080,55 @@
1080 ** All other parameters are for internal use only, submitted via the
1081 ** form-submission process, and may change with any given revision of
1082 ** this code.
1083 */
1084 void fileedit_page(){
1085 const char * zFilename = PD("file",P("name"));
1086 /* filename. We'll accept 'name'
1087 because that param is handled
1088 specially by the core. */
1089 const char * zRev = P("r"); /* checkin version */
1090 const char * zContent = P("content"); /* file content */
1091 const char * zComment = P("comment"); /* checkin comment */
1092 CheckinMiniInfo cimi; /* Checkin state */
1093 int submitMode = 0; /* See mapping below */
1094 int vid, newVid = 0; /* checkin rid */
 
1095 int frid = 0; /* File content rid */
1096 int previewLn = P("preview_ln")!=0; /* Line number mode */
1097 char * zFileUuid = 0; /* File content UUID */
1098 Blob err = empty_blob; /* Error report */
1099 const char * zFlagCheck = 0; /* Temp url flag holder */
1100 Blob endScript = empty_blob; /* Script code to run at the
1101 end. This content will be
1102 combined into a single JS
1103 function call, thus each
1104 entry must end with a
1105 semicolon. */
1106 Stmt stmt = empty_Stmt;
1107 const int loadMode = 0; /* See next comment block */
1108 /* loadMode: How to populate the TEXTAREA:
1109 **
1110 ** 0: HTML encode: despite my personal reservations regarding HTML
1111 ** escaping, this seems to be the only reliable approach
1112 ** until/unless we completely AJAXify this page.
1113 **
1114 ** 1: JSON mode: JSON-izes the file content and injects it, via JS,
1115 ** into the editor TEXTAREA. This works wonderfully until the input
1116 ** file contains an raw <SCRIPT> tag, at which points the HTML
1117 ** parser chokes on it.
1118 **
1119 ** 2: AJAX mode: can only load content from the db, not preview/dry-run
1120 ** content. Unless this page is refactored to work solely over AJAX
1121 ** (which is a potential TODO), this method is only useful on the
1122 ** initial hit to this page, where the file is loaded.
1123 **
1124 ** loadMode is not generally configurable: change it only for
1125 ** testing/development purposes.
1126 */
1127 #define fail(EXPR) blob_appendf EXPR; goto end_footer
1128
1129 assert(loadMode==0 || loadMode==1 || loadMode==2);
1130 login_check_credentials();
1131 if( !g.perm.Write ){
1132 login_needed(g.anon.Write);
1133 return;
1134 }
@@ -1023,11 +1148,12 @@
1148 /* As of this point, don't use return or fossil_fatal(), use
1149 ** fail((&err,...)) instead so that we can be sure to do any
1150 ** cleanup and end the transaction cleanly.
1151 */
1152 if(!zRev || !*zRev || !zFilename || !*zFilename){
1153 fail((&err,"Missing required URL parameters: "
1154 "file=FILE and r=CHECKIN"));
1155 }
1156 if(0==fileedit_is_editable(zFilename)){
1157 fail((&err,"Filename <code>%h</code> is disallowed "
1158 "by the <code>fileedit-glob</code> repository "
1159 "setting.",
@@ -1035,10 +1161,11 @@
1161 }
1162 vid = symbolic_name_to_rid(zRev, "ci");
1163 if(0==vid){
1164 fail((&err,"Could not resolve checkin version."));
1165 }
1166 cimi.zFilename = mprintf("%s",zFilename);
1167
1168 /* Find the repo-side file entry or fail... */
1169 cimi.zParentUuid = rid_to_uuid(vid);
1170 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1171 "WHERE filename=%Q %s AND checkinID=%d",
@@ -1075,13 +1202,18 @@
1202
1203 /* All set. Here we go... */
1204
1205 CX("<h1>Editing:</h1>");
1206 CX("<p class='fileedit-hint'>");
1207 CX("File: "
1208 "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
1209 "<code>%h</code><br>",
1210 zFilename, zFileUuid, zFilename);
1211 CX("Checkin Version: "
1212 "[<a id='r-link' href='%R/info/%!S'>info</a>] "
1213 "<code id='r-label'>%s</code><br>",
1214 cimi.zParentUuid, cimi.zParentUuid);
1215 CX("Permalink: <code>"
1216 "<a id='permalink' href='%R/fileedit?file=%T&r=%!S'>"
1217 "/fileedit?file=%T&r=%!S</a></code><br>"
1218 "(Clicking the permalink will reload the page and discard "
1219 "all edits!)",
@@ -1090,12 +1222,13 @@
1222 CX("</p>");
1223 CX("<p>This page is <em>far from complete</em> and may still have "
1224 "significant bugs. USE AT YOUR OWN RISK, preferably on a test "
1225 "repo.</p>\n");
1226
1227 CX("<form action='%R/fileedit%s' method='POST' "
1228 "class='fileedit-form'>\n",
1229 submitMode>0 ? "#options" : "");
1230
1231 /******* Hidden fields *******/
1232 CX("<input type='hidden' name='r' value='%s'>",
1233 cimi.zParentUuid);
1234 CX("<input type='hidden' name='file' value='%T'>",
@@ -1113,14 +1246,19 @@
1246
1247 /******* Content *******/
1248 CX("<h3>File Content</h3>\n");
1249 CX("<textarea name='content' id='fileedit-content' "
1250 "rows='20' cols='80'>");
1251 if(0==loadMode){
1252 CX("%h",blob_str(&cimi.fileContent));
1253 }else{
1254 CX("Loading...");
1255 /* Performed via JS later on */
1256 }
1257 CX("</textarea>\n");
1258 /******* Flags/options *******/
1259 CX("<fieldset class='fileedit-options' id='options'>"
1260 "<legend>Options</legend><div>"
1261 /* Chrome does not sanely lay out multiple
1262 ** fieldset children after the <legend>, so
1263 ** a containing div is necessary. */);
1264 /*
@@ -1192,36 +1330,33 @@
1330 CX("</select>");
1331 }
1332
1333 CX("</div></fieldset>") /* end of checkboxes */;
1334
1335 /******* Buttons *******/
1336 CX("<a id='buttons'></a>");
1337 CX("<fieldset class='fileedit-options'>"
1338 "<legend>Tell the server to...</legend><div>");
1339 CX("<button type='submit' name='submit' value='1'>"
1340 "Save</button>");
1341 CX("<button type='submit' name='submit' value='2'>"
1342 "Preview</button>");
1343 CX("<button type='submit' name='submit' value='3'>"
1344 "Diff (TODO)</button>");
1345 CX("<br>");
1346 style_labeled_checkbox("preview_ln",
1347 "Add line numbers to plain-text previews?", "1",
1348 "If on, plain-text files (only) will get "
1349 "line numbers added to the preview.",
1350 previewLn);
1351 CX("</div></fieldset>");
1352
1353 /******* End of form *******/
1354 CX("</form>\n");
1355
1356 /* Dynamically populate the editor... */
1357 if(1==loadMode || (2==loadMode && submitMode>0)){
 
 
 
 
 
 
 
 
 
 
1358 char const * zQuoted = 0;
1359 if(blob_size(&cimi.fileContent)>0){
1360 db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1361 db_step(&stmt);
1362 zQuoted = db_column_text(&stmt,0);
@@ -1231,10 +1366,21 @@
1366 "document.getElementById('fileedit-content')"
1367 ".value=%s;", zQuoted ? zQuoted : "'';\n");
1368 if(stmt.pStmt){
1369 db_finalize(&stmt);
1370 }
1371 }else if(2==loadMode){
1372 assert(submitMode==0);
1373 fileedit_emit_script_fetch();
1374 blob_appendf(&endScript,
1375 "window.fossilFetch('raw/%s',{"
1376 "onload: (r)=>document.getElementById('fileedit-content')"
1377 ".value=r,"
1378 "onerror:()=>document.getElementById('fileedit-content')"
1379 ".value="
1380 "'Error loading content'"
1381 "});\n", zFileUuid);
1382 }
1383
1384 if(1==submitMode/*save*/){
1385 Blob manifest = empty_blob;
1386 char * zNewUuid = 0;
@@ -1244,11 +1390,10 @@
1390 }else{
1391 fail((&err,"Empty comment is not permitted."));
1392 }
1393 /*cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
1394 assert(cimi.pParent && "We know vid is valid.");*/
 
1395 cimi.pMfOut = &manifest;
1396 checkin_mini(&cimi, &newVid, &err);
1397 if(newVid!=0){
1398 zNewUuid = rid_to_uuid(newVid);
1399 CX("<h3>Manifest%s: %S</h3><pre>"
@@ -1269,31 +1414,42 @@
1414 blob_appendf(&endScript,
1415 "/* Update version number */\n"
1416 "document.querySelector('input[name=r]')"
1417 ".value=%Q;\n"
1418 "document.querySelector('#r-label')"
1419 ".innerText=%Q;\n"
1420 "document.querySelector('#r-link')"
1421 ".setAttribute('href', '%R/info/%!S');\n"
1422 "document.querySelector('#finfo-link')"
1423 ".setAttribute('href','%R/finfo?name=%T&m=%!S');\n",
1424 /*input[name=r]:*/zNewUuid, /*#r-label:*/ zNewUuid,
1425 /*#r-link:*/ zNewUuid,
1426 /*#finfo-link:*/zFilename, zNewUuid);
1427 blob_appendf(&endScript,
1428 "/* Updated finfo link */"
1429 );
1430 blob_appendf(&endScript,
1431 "/* Update permalink */\n"
1432 "const urlFull='%R/fileedit?file=%T&r=%!S';\n"
1433 "const urlShort='/fileedit?file=%T&r=%!S';\n"
1434 "let link=document.querySelector('#permalink');\n"
1435 "link.innerText=urlShort;\n"
1436 "link.setAttribute('href',urlFull);\n",
1437 cimi.zFilename, zNewUuid,
1438 cimi.zFilename, zNewUuid);
1439 }
1440 fossil_free(zNewUuid);
1441 zNewUuid = 0;
1442 }
1443 /* On error, the error message is in the err blob and will
1444 ** be emitted below. */
1445 cimi.pMfOut = 0;
1446 blob_reset(&manifest);
1447 }else if(2==submitMode/*preview*/){
1448 int pflags = 0;
1449 if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1450 fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags);
1451 }else if(3==submitMode/*diff*/){
1452 fail((&err,"Diff mode is still TODO."));
1453 }else{
1454 /* Ignore invalid submitMode value */
1455 goto end_footer;
1456

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button