Fossil SCM

Moved some generic fileedit code to style.c. Refactored /fileedit to not require JS to update version info, making this impl pure no-JS. Now to ajaxify it...

stephan 2020-05-04 23:26 checkin-without-checkout
Commit 8d4ce834ed9bd423005ead7281aa36cb212260d45448e1dc3d509784ce02d64f
2 files changed +237 -294 +134
+237 -294
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,61 +898,97 @@
898898
fossil_free(zGlobs);
899899
}
900900
return glob_match(pGlobs, zFilename);
901901
}
902902
903
-static void fileedit_emit_script(int phase){
904
- if(0==phase){
905
- CX("<script nonce='%s'>", style_nonce());
906
- }else{
907
- CX("</script>\n");
908
- }
909
-}
910
-
911903
/*
912
-** Emits a script tag which defines window.fossilFetch(), which works
904
+** Emits a script tag which defines window.fossil.fetch(), which works
913905
** similarly (not identically) to the not-quite-ubiquitous global
914906
** fetch().
915907
**
916908
** JS usages:
917909
**
918910
** fossilFetch( URI, onLoadCallback );
919911
**
920912
** fossilFetch( URI, optionsObject );
921913
**
922
-** Where the optionsObject may be an object with any of these
923
-** properties:
914
+** Noting that URI must be relative to the top of the repository and
915
+** must not start with a slash (if it does, it is stripped). It gets
916
+** %R/ prepended to it.
917
+**
918
+** The optionsObject may be an onload callback or an object with any
919
+** of these properties:
924920
**
925921
** - onload: callback(responseData) (default = output response to
926922
** console).
927923
**
928
-** - onerror: callback(XHR onload event) (default = no-op)
924
+** - onerror: callback(XHR onload event) (default = console output)
929925
**
930926
** - method: 'POST' | 'GET' (default = 'GET')
931927
**
932
-** Noting that URI must be relative to the top of the repository and
933
-** must not start with a slash. It gets %R/ prepended to it.
934
-**
935
-** TODOs, if needed, include:
936
-**
937
-** optionsObject.params: object map of key/value pairs to append to the
938
-** URI.
939
-**
940
-** optionsObject.payload: string or JSON-able object to POST as the
941
-** payload.
928
+** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
929
+** Document, FormData, Blob, File, ArrayBuffer), or a plain object
930
+** or array, either of which gets JSON.stringify()'d. If set then
931
+** the method is automatically set to 'POST'. If an object/array is
932
+** converted to JSON, the content-type is set to 'application/json'.
933
+** By default XHR2 will set the content type based on the payload
934
+** type.
935
+**
936
+** - contentType: Optional request content type when POSTing. Ignored
937
+** if the method is not 'POST'.
938
+**
939
+** - responseType: optional string. One of ("text", "arraybuffer",
940
+** "blob", or "document") (as specified by XHR2). Default = "text".
941
+**
942
+** - urlParams: string|object. If a string, it is assumed to be a
943
+** URI-encoded list of params in the form "key1=val1&key2=val2...",
944
+** with NO leading '?'. If it is an object, all of its properties
945
+** get converted to that form. Either way, the parameters get
946
+** appended to the URL.
942947
**
943948
*/
944
-static void fileedit_emit_script_fetch(){
945
- fileedit_emit_script(0);
946
- CX("window.fossilFetch = function(path,opt){\n");
947
- CX(" if('function'===typeof opt){\n");
948
- CX(" opt={onload:opt};\n");
949
- CX(" }else{\n");
950
- CX(" opt=opt||{onload:function(r){console.debug('response:',r)}}\n");
949
+void fileedit_emit_script_fetch(){
950
+ style_emit_script_tag(0);
951
+ CX("fossil.fetch = function(path,opt){\n");
952
+ CX(" if('/'===path[0]) path = path.substr(1);\n");
953
+ CX(" if(!opt){\n");
954
+ CX(" opt = {onload:(r)=>console.debug('response:',r)};\n");
955
+ CX(" }else if('function'===typeof opt){\n");
956
+ CX(" opt={onload:opt,\n");
957
+ CX(" onerror:(e)=>console.error('ajax error:',e)};\n");
958
+ CX(" }\n");
959
+ CX(" let payload = opt.payload;\n");
960
+ CX(" if(payload){\n");
961
+ CX(" opt.method = 'POST';\n");
962
+ CX(" if(!(payload instanceof FormData)\n");
963
+ CX(" && !(payload instanceof Document)\n");
964
+ CX(" && !(payload instanceof Blob)\n");
965
+ CX(" && !(payload instanceof File)\n");
966
+ CX(" && !(payload instanceof ArrayBuffer)){\n");
967
+ CX(" if('object'===typeof payload || payload instanceof Array){\n");
968
+ CX(" payload = JSON.stringify(payload);\n");
969
+ CX(" opt.contentType = 'application/json';\n");
970
+ CX(" }\n");
971
+ CX(" }\n");
972
+ CX(" }\n");
973
+ CX(" const url=['%R/'+path], x=new XMLHttpRequest();\n");
974
+ CX(" if(opt.urlParams){\n");
975
+ CX(" url.push('?');\n");
976
+ CX(" if('string'===typeof opt.urlParams){\n");
977
+ CX(" url.push(opt.urlParams);\n");
978
+ CX(" }else{/*assume object*/\n");
979
+ CX(" let k, i = 0;\n");
980
+ CX(" for( k in opt.urlParams ){\n");
981
+ CX(" if(i++) url.push('&');\n");
982
+ CX(" url.push(k,'=',encodeURIComponent(opt.urlParams[k]));\n");
983
+ CX(" }\n");
984
+ CX(" }\n");
985
+ CX(" }\n");
986
+ CX(" if('POST'===opt.method && 'string'===typeof opt.contentType){\n");
987
+ CX(" x.setRequestHeader('Content-Type',opt.contentType);\n");
951988
CX(" }\n");
952
- CX(" const url='%R/'+path, x=new XMLHttpRequest();\n");
953
- CX(" x.open(opt.method||'GET', url, true);\n");
989
+ CX(" x.open(opt.method||'GET', url.join(''), true);\n");
954990
CX(" x.responseType=opt.responseType||'text';\n");
955991
CX(" if(opt.onload){\n");
956992
CX(" x.onload = function(e){\n");
957993
CX(" if(200!==this.status){\n");
958994
CX(" if(opt.onerror) opt.onerror(e);\n");
@@ -959,40 +995,16 @@
959995
CX(" return;\n");
960996
CX(" }\n");
961997
CX(" opt.onload(this.response);\n");
962998
CX(" }\n");
963999
CX(" }\n");
964
- CX(" x.send();");
1000
+ CX(" if(payload) x.send(payload);\n");
1001
+ CX(" else x.send();\n");
9651002
CX("};\n");
966
- fileedit_emit_script(1);
1003
+ style_emit_script_tag(1);
9671004
};
9681005
969
-/*
970
-** Outputs a labeled checkbox element:
971
-**
972
-** <span class='input-with-label' title={{zTip}}>
973
-** <input type='checkbox' name={{zFieldName}} value={{zValue}}
974
-** {{isChecked ? " checked : ""}}/>
975
-** <span>{{zLabel}}</span>
976
-** </span>
977
-**
978
-** zFieldName, zLabel, and zValue are required. zTip is optional.
979
-*/
980
-static void style_labeled_checkbox(const char *zFieldName,
981
- const char * zLabel,
982
- const char * zValue,
983
- const char * zTip,
984
- int isChecked){
985
- CX("<div class='input-with-label'");
986
- if(zTip && *zTip){
987
- CX(" title='%h'", zTip);
988
- }
989
- CX("><input type='checkbox' name='%s' value='%T'%s/>",
990
- zFieldName,
991
- zValue ? zValue : "", isChecked ? " checked" : "");
992
- CX("<span>%h</span></div>", zLabel);
993
-}
9941006
9951007
enum fileedit_render_preview_flags {
9961008
FE_PREVIEW_LINE_NUMBERS = 1
9971009
};
9981010
enum fileedit_render_modes {
@@ -1030,15 +1042,16 @@
10301042
CX("<div class='fileedit-preview'>");
10311043
CX("<div>Preview</div>");
10321044
switch(renderMode){
10331045
case FE_RENDER_HTML:{
10341046
char * z64 = encode64(blob_str(pContent), blob_size(pContent));
1035
- CX("<iframe width='100%%' frameborder='0' marginwidth='0' "
1036
- "style='height:%dem' "
1037
- "marginheight='0' sandbox='allow-same-origin' id='ifm1' "
1038
- "src='data:text/html;base64,%z'"
1039
- "></iframe>", nIframeHeightEm ? nIframeHeightEm : 40,
1047
+ CX("<iframe width='100%%' frameborder='0' "
1048
+ "marginwidth='0' style='height:%dem' "
1049
+ "marginheight='0' sandbox='allow-same-origin' "
1050
+ "id='ifm1' src='data:text/html;base64,%z'"
1051
+ "></iframe>",
1052
+ nIframeHeightEm ? nIframeHeightEm : 40,
10401053
z64);
10411054
break;
10421055
}
10431056
case FE_RENDER_WIKI:
10441057
wiki_render_by_mimetype(pContent, zMime);
@@ -1061,27 +1074,22 @@
10611074
}
10621075
10631076
/*
10641077
** Renders diffs for the /fileedit page. pContent is the
10651078
** locally-edited content. frid is the RID of the file's blob entry
1066
-** from which pContent is based. zManifestUuid is the checkin version
1079
+** from which pContent is based. zManifestUuid is the checkin version
10671080
** to which RID belongs - it is purely informational, for labeling the
10681081
** diff view. isSbs is true for side-by-side diffs, false for unified.
10691082
*/
10701083
static void fileedit_render_diff(Blob * pContent, int frid,
10711084
const char * zManifestUuid,
10721085
int isSbs){
10731086
Blob orig = empty_blob;
10741087
Blob out = empty_blob;
1075
- u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR;
1076
-
1088
+ u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
1089
+ | (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
10771090
content_get(frid, &orig);
1078
- if(isSbs){
1079
- diffFlags |= DIFF_SIDEBYSIDE;
1080
- }else{
1081
- diffFlags |= DIFF_LINENO;
1082
- }
10831091
text_diff(&orig, pContent, &out, 0, diffFlags);
10841092
CX("<div class='fileedit-diff'>");
10851093
CX("<div>Diff <code>[%S]</code> &rarr; Local Edits</div>",
10861094
zManifestUuid);
10871095
if(isSbs){
@@ -1090,91 +1098,33 @@
10901098
CX("<pre class='udiff'>%b</pre>",&out);
10911099
}
10921100
CX("</div><!--.fileedit-diff-->\n");
10931101
blob_reset(&orig);
10941102
blob_reset(&out);
1095
- /* Wow, that was *easy*. */
1096
-}
1097
-
1098
-/*
1099
-** Outputs a SELECT list from a compile-time list of integers.
1100
-** The vargs must be a list of (const char *, int) pairs, terminated
1101
-** with a single NULL. Each pair is interpreted as...
1102
-**
1103
-** If the (const char *) is NULL, it is the end of the list, else
1104
-** a new OPTION entry is created. If the string is empty, the
1105
-** label and value of the OPTION is the integer part of the pair.
1106
-** If the string is not empty, it becomes the label and the integer
1107
-** the value. If that value == selectedValue then that OPTION
1108
-** element gets the 'selected' attribute.
1109
-**
1110
-** Note that the pairs are not in (int, const char *) order because
1111
-** there is no well-known integer value which we can definitively use
1112
-** as a list terminator.
1113
-**
1114
-** zFieldName is the value of the form element's name attribute.
1115
-**
1116
-** zLabel is an optional string to use as a "label" for the element
1117
-** (see below).
1118
-**
1119
-** zTooltip is an optional value for the SELECT's title attribute.
1120
-**
1121
-** The structure of the emited HTML is:
1122
-**
1123
-** <div class='input-with-label'>
1124
-** <span>{{zLabel}}</span>
1125
-** <select>...</select>
1126
-** </div>
1127
-**
1128
-*/
1129
-static void style_select_list_int_v(const char *zFieldName,
1130
- const char * zLabel,
1131
- const char * zToolTip,
1132
- int selectedVal, va_list vargs){
1133
- CX("<div class='input-with-label'");
1134
- if(zToolTip && *zToolTip){
1135
- CX(" title='%h'",zToolTip);
1136
- }
1137
- CX(">");
1138
- if(zLabel && *zLabel){
1139
- CX("<span>%h</span>", zLabel);
1140
- }
1141
- CX("<select name='%s'>",zFieldName);
1142
- while(1){
1143
- const char * zOption = va_arg(vargs,char *);
1144
- int v;
1145
- if(NULL==zOption){
1146
- break;
1147
- }
1148
- v = va_arg(vargs,int);
1149
- CX("<option value='%d'%s>",
1150
- v, v==selectedVal ? " selected" : "");
1151
- if(*zOption){
1152
- CX("%s", zOption);
1153
- }else{
1154
- CX("%d",v);
1155
- }
1156
- CX("</option>\n");
1157
- }
1158
- CX("</select>\n");
1159
- if(zLabel && *zLabel){
1160
- CX("</div>\n");
1161
- }
1162
-}
1163
-
1164
-/*
1165
-** The ellipsis-args counterpart of style_select_list_int_v().
1166
-*/
1167
-void style_select_list_int(const char *zFieldName,
1168
- const char * zLabel,
1169
- const char * zToolTip,
1170
- int selectedVal, ... ){
1171
- va_list vargs;
1172
- va_start(vargs,selectedVal);
1173
- style_select_list_int_v(zFieldName, zLabel, zToolTip,
1174
- selectedVal, vargs);
1175
- va_end(vargs);
1103
+}
1104
+
1105
+/*
1106
+** Given a repo-relative filename and a manifest RID, returns the UUID
1107
+** of the corresponding file entry. Returns NULL if no match is
1108
+** found. If pFilePerm is not NULL, the file's permission flag value
1109
+** is written to *pFilePerm.
1110
+*/
1111
+static char *fileedit_file_uuid(char const *zFilename,
1112
+ int vid, int *pFilePerm){
1113
+ Stmt stmt = empty_Stmt;
1114
+ char * zFileUuid = 0;
1115
+ db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1116
+ "WHERE filename=%Q %s AND checkinID=%d",
1117
+ zFilename, filename_collation(), vid);
1118
+ if(SQLITE_ROW==db_step(&stmt)){
1119
+ zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
1120
+ if(pFilePerm){
1121
+ *pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1));
1122
+ }
1123
+ }
1124
+ db_finalize(&stmt);
1125
+ return zFileUuid;
11761126
}
11771127
11781128
/*
11791129
** WEBPAGE: fileedit
11801130
**
@@ -1212,10 +1162,11 @@
12121162
int previewLn = P("preview_ln")!=0; /* Line number mode */
12131163
int previewHtmlHeight = 0; /* iframe height (EMs) */
12141164
int previewRenderMode = FE_RENDER_GUESS; /* preview mode */
12151165
char * zFileUuid = 0; /* File content UUID */
12161166
Blob err = empty_blob; /* Error report */
1167
+ Blob submitResult = empty_blob; /* Error report */
12171168
const char * zFlagCheck = 0; /* Temp url flag holder */
12181169
Blob endScript = empty_blob; /* Script code to run at the
12191170
end. This content will be
12201171
combined into a single JS
12211172
function call, thus each
@@ -1285,27 +1236,19 @@
12851236
cimi.zFilename = mprintf("%s",zFilename);
12861237
zFileMime = mimetype_from_name(zFilename);
12871238
12881239
/* Find the repo-side file entry or fail... */
12891240
cimi.zParentUuid = rid_to_uuid(vid);
1290
- db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1291
- "WHERE filename=%Q %s AND checkinID=%d",
1292
- zFilename, filename_collation(), vid);
1293
- if(SQLITE_ROW==db_step(&stmt)){
1294
- const char * zPerm = db_column_text(&stmt, 1);
1295
- cimi.filePerm = mfile_permstr_int(zPerm);
1296
- if(PERM_LNK==cimi.filePerm){
1297
- fail((&err,"Editing symlinks is not permitted."));
1298
- }
1299
- zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
1300
- }
1301
- db_finalize(&stmt);
1241
+ zFileUuid = fileedit_file_uuid(zFilename, vid, &cimi.filePerm);
13021242
if(!zFileUuid){
13031243
fail((&err,"Checkin [%S] does not contain file: "
13041244
"<code>%h</code>",
13051245
cimi.zParentUuid, zFilename));
13061246
}
1247
+ else if(PERM_LNK==cimi.filePerm){
1248
+ fail((&err,"Editing symlinks is not permitted."));
1249
+ }
13071250
frid = fast_uuid_to_rid(zFileUuid);
13081251
assert(frid);
13091252
13101253
/* Read file content from submit request or repo... */
13111254
if(zContent==0){
@@ -1318,11 +1261,100 @@
13181261
if(looks_like_binary(&cimi.fileContent)){
13191262
fail((&err,"File appears to be binary. Cannot edit: "
13201263
"<code>%h</code>",zFilename));
13211264
}
13221265
1323
- /* All set. Here we go... */
1266
+ /*
1267
+ ** TODO?: date-override date selection field. Maybe use
1268
+ ** an input[type=datetime-local].
1269
+ */
1270
+ if(SUBMIT_NONE==submitMode || P("dry_run")!=0){
1271
+ cimi.flags |= CIMINI_DRY_RUN;
1272
+ }
1273
+ if(P("allow_fork")!=0){
1274
+ cimi.flags |= CIMINI_ALLOW_FORK;
1275
+ }
1276
+ if(P("allow_older")!=0){
1277
+ cimi.flags |= CIMINI_ALLOW_OLDER;
1278
+ }
1279
+ if(P("exec_bit")!=0){
1280
+ cimi.filePerm = PERM_EXE;
1281
+ }
1282
+ if(P("allow_merge_conflict")!=0){
1283
+ cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
1284
+ }
1285
+ if(P("prefer_delta")!=0){
1286
+ cimi.flags |= CIMINI_PREFER_DELTA;
1287
+ }
1288
+ /* EOL conversion policy... */
1289
+ {
1290
+ const int eolMode = submitMode==SUBMIT_NONE
1291
+ ? 0 : atoi(PD("eol","0"));
1292
+ switch(eolMode){
1293
+ case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
1294
+ case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
1295
+ default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
1296
+ }
1297
+ }
1298
+
1299
+ /********************************************************************
1300
+ ** All errors which "could" have happened up to this point are of a
1301
+ ** degree which keep us from rendering the rest of the page, and
1302
+ ** thus fail() has already skipped to the end of the page to render
1303
+ ** the errors. Any up-coming errors, barring malloc failure or
1304
+ ** similar, are not "that" fatal. We can/should continue rendering
1305
+ ** the page, then output the error message at the end.
1306
+ **
1307
+ ** Because we cannot intercept the output of the PREVIEW and DIFF
1308
+ ** rendering, we have to delay the "real work" for those modes until
1309
+ ** after the rest of the page has been rendered. In the case of
1310
+ ** SAVE, we can capture all of the output, and thus can perform that
1311
+ ** work before rendering, which is important so that we have the
1312
+ ** proper version information when rendering the rest of the page.
1313
+ ********************************************************************/
1314
+#undef fail
1315
+ while(SUBMIT_SAVE==submitMode){
1316
+ Blob manifest = empty_blob;
1317
+ /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
1318
+ if(zComment && *zComment){
1319
+ blob_append(&cimi.comment, zComment, -1);
1320
+ }else{
1321
+ blob_append(&err,"Empty checkin comment is not permitted.",-1);
1322
+ break;
1323
+ }
1324
+ cimi.pMfOut = &manifest;
1325
+ checkin_mini(&cimi, &newVid, &err);
1326
+ if(newVid!=0){
1327
+ char * zNewUuid = rid_to_uuid(newVid);
1328
+ blob_appendf(&submitResult,
1329
+ "<h3>Manifest%s: %S</h3><pre>"
1330
+ "<code class='fileedit-manifest'>%h</code>"
1331
+ "</pre>",
1332
+ (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
1333
+ zNewUuid, blob_str(&manifest));
1334
+ if(CIMINI_DRY_RUN & cimi.flags){
1335
+ fossil_free(zNewUuid);
1336
+ }else{
1337
+ /* Update cimi version info... */
1338
+ assert(cimi.pParent);
1339
+ assert(cimi.zParentUuid);
1340
+ fossil_free(zFileUuid);
1341
+ zFileUuid = fileedit_file_uuid(cimi.zFilename, newVid, 0);
1342
+ manifest_destroy(cimi.pParent);
1343
+ cimi.pParent = 0;
1344
+ fossil_free(cimi.zParentUuid);
1345
+ cimi.zParentUuid = zNewUuid;
1346
+ zComment = 0;
1347
+ cimi.flags |= CIMINI_DRY_RUN /* for sanity's sake */;
1348
+ }
1349
+ }
1350
+ /* On error, the error message is in the err blob and will
1351
+ ** be emitted at the end. */
1352
+ cimi.pMfOut = 0;
1353
+ blob_reset(&manifest);
1354
+ break;
1355
+ }
13241356
13251357
CX("<h1>Editing:</h1>");
13261358
CX("<p class='fileedit-hint'>");
13271359
CX("File: "
13281360
"[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
@@ -1338,12 +1370,12 @@
13381370
"(Clicking the permalink will reload the page and discard "
13391371
"all edits!)",
13401372
zFilename, cimi.zParentUuid,
13411373
zFilename, cimi.zParentUuid);
13421374
CX("</p>");
1343
- CX("<p>This page is <em>far from complete</em> and may still have "
1344
- "significant bugs. USE AT YOUR OWN RISK, preferably on a test "
1375
+ CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
1376
+ "USE AT YOUR OWN RISK, preferably on a test "
13451377
"repo.</p>\n");
13461378
13471379
CX("<form action='%R/fileedit#options' method='POST' "
13481380
"class='fileedit'>\n");
13491381
@@ -1368,78 +1400,49 @@
13681400
CX("<fieldset class='fileedit-options' id='options'>"
13691401
"<legend>Options</legend><div>"
13701402
/* Chrome does not sanely lay out multiple
13711403
** fieldset children after the <legend>, so
13721404
** a containing div is necessary. */);
1373
- /*
1374
- ** TODO?: date-override date selection field. Maybe use
1375
- ** an input[type=datetime-local].
1376
- */
1377
- if(SUBMIT_NONE==submitMode || P("dry_run")!=0){
1378
- cimi.flags |= CIMINI_DRY_RUN;
1379
- }
13801405
style_labeled_checkbox("dry_run", "Dry-run?", "1",
13811406
"In dry-run mode, the Save button performs "
13821407
"all work needed for saving but then rolls "
13831408
"back the transaction, and thus does not "
13841409
"really save.",
13851410
cimi.flags & CIMINI_DRY_RUN);
1386
- if(P("allow_fork")!=0){
1387
- cimi.flags |= CIMINI_ALLOW_FORK;
1388
- }
13891411
style_labeled_checkbox("allow_fork", "Allow fork?", "1",
13901412
"Allow saving to create a fork?",
13911413
cimi.flags & CIMINI_ALLOW_FORK);
1392
- if(P("allow_older")!=0){
1393
- cimi.flags |= CIMINI_ALLOW_OLDER;
1394
- }
13951414
style_labeled_checkbox("allow_older", "Allow older?", "1",
13961415
"Allow saving against a parent version "
13971416
"which has a newer timestamp?",
13981417
cimi.flags & CIMINI_ALLOW_OLDER);
1399
- if(P("exec_bit")!=0){
1400
- cimi.filePerm = PERM_EXE;
1401
- }
14021418
style_labeled_checkbox("exec_bit", "Executable?", "1",
14031419
"Set the executable bit?",
14041420
PERM_EXE==cimi.filePerm);
1405
- if(P("allow_merge_conflict")!=0){
1406
- cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
1407
- }
14081421
style_labeled_checkbox("allow_merge_conflict",
14091422
"Allow merge conflict markers?", "1",
14101423
"Allow saving even if the content contains "
14111424
"what appear to be fossil merge conflict "
14121425
"markers?",
14131426
cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
1414
- if(P("prefer_delta")!=0){
1415
- cimi.flags |= CIMINI_PREFER_DELTA;
1416
- }
14171427
style_labeled_checkbox("prefer_delta",
14181428
"Prefer delta manifest?", "1",
14191429
"Will create a delta manifest, instead of "
14201430
"baseline, if conditions are favorable to do "
14211431
"so. This option is only a suggestion.",
14221432
cimi.flags & CIMINI_PREFER_DELTA);
1423
- {/* EOL conversion policy... */
1424
- const int eolMode = submitMode==SUBMIT_NONE
1425
- ? 0 : atoi(PD("eol","0"));
1426
- switch(eolMode){
1427
- case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
1428
- case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
1429
- default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
1430
- }
1431
- style_select_list_int("eol", "EOL Style",
1432
- "EOL conversion policy, noting that "
1433
- "form-processing may implicitly change the "
1434
- "line endings of the input.",
1435
- eolMode==1||eolMode==2 ? eolMode : 0,
1436
- "Inherit", 0,
1437
- "Unix", 1,
1438
- "Windows", 2,
1439
- NULL);
1440
- }
1433
+ style_select_list_int("eol", "EOL Style",
1434
+ "EOL conversion policy, noting that "
1435
+ "form-processing may implicitly change the "
1436
+ "line endings of the input.",
1437
+ (cimi.flags & CIMINI_CONVERT_EOL_UNIX)
1438
+ ? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS
1439
+ ? 2 : 0),
1440
+ "Inherit", 0,
1441
+ "Unix", 1,
1442
+ "Windows", 2,
1443
+ NULL);
14411444
14421445
CX("</div></fieldset>") /* end of checkboxes */;
14431446
14441447
/******* Comment *******/
14451448
CX("<a id='comment'></a>");
@@ -1446,25 +1449,23 @@
14461449
CX("<fieldset><legend>Commit message</legend><div>");
14471450
CX("<textarea name='comment' rows='3' cols='80'>");
14481451
/* ^^^ adding the 'required' attribute means we cannot even submit
14491452
** for PREVIEW mode if it's empty :/. */
14501453
if(zComment && *zComment){
1451
- CX("%h"/*%h? %s?*/, zComment);
1454
+ CX("%h", zComment);
14521455
}
14531456
CX("</textarea>\n");
14541457
CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup "
14551458
"syntax.</div>\n"/*TODO: select for fossil/md/plain text*/);
14561459
CX("</div></fieldset>\n");
14571460
1458
-
1459
-
14601461
/******* Buttons *******/
14611462
CX("<a id='buttons'></a>");
14621463
CX("<fieldset class='fileedit-options'>"
14631464
"<legend>Tell the server to...</legend><div>");
14641465
CX("<button type='submit' name='submit' value='%d'>"
1465
- "Save</button>", SUBMIT_SAVE);
1466
+ "Commit</button>", SUBMIT_SAVE);
14661467
CX("<button type='submit' name='submit' value='%d'>"
14671468
"Preview</button>", SUBMIT_PREVIEW);
14681469
{
14691470
/* Preview rendering mode selection... */
14701471
previewRenderMode = atoi(PD("preview_render_mode","0"));
@@ -1512,12 +1513,29 @@
15121513
"Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED);
15131514
CX("</div></fieldset>");
15141515
15151516
/******* End of form *******/
15161517
CX("</form>\n");
1518
+
1519
+ /*
1520
+ ** We cannot intercept the output for PREVIEW
1521
+ ** and DIFF modes, and therefore have to render those
1522
+ ** last.
1523
+ */
1524
+ if(SUBMIT_PREVIEW==submitMode){
1525
+ int pflags = 0;
1526
+ if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1527
+ fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1528
+ previewRenderMode, previewHtmlHeight);
1529
+ }else if(SUBMIT_DIFF_SBS==submitMode
1530
+ || SUBMIT_DIFF_UNIFIED==submitMode){
1531
+ fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1532
+ SUBMIT_DIFF_SBS==submitMode);
1533
+ }
15171534
15181535
/* Dynamically populate the editor... */
1536
+ fileedit_emit_script_fetch();
15191537
if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){
15201538
char const * zQuoted = 0;
15211539
if(blob_size(&cimi.fileContent)>0){
15221540
db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
15231541
db_step(&stmt);
@@ -1530,117 +1548,42 @@
15301548
if(stmt.pStmt){
15311549
db_finalize(&stmt);
15321550
}
15331551
}else if(2==loadMode){
15341552
assert(submitMode==SUBMIT_NONE);
1535
- fileedit_emit_script_fetch();
15361553
blob_appendf(&endScript,
1537
- "window.fossilFetch('raw/%s',{"
1554
+ "window.fossil.fetch('raw/%s',{"
15381555
"onload: (r)=>document.getElementById('fileedit-content')"
15391556
".value=r,"
15401557
"onerror:()=>document.getElementById('fileedit-content')"
15411558
".value="
15421559
"'Error loading content'"
15431560
"});\n", zFileUuid);
15441561
}
15451562
1546
- if(SUBMIT_SAVE==submitMode){
1547
- Blob manifest = empty_blob;
1548
- char * zNewUuid = 0;
1549
- /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
1550
- if(zComment && *zComment){
1551
- blob_append(&cimi.comment, zComment, -1);
1552
- }else{
1553
- fail((&err,"Empty comment is not permitted."));
1554
- }
1555
- /*cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
1556
- assert(cimi.pParent && "We know vid is valid.");*/
1557
- cimi.pMfOut = &manifest;
1558
- checkin_mini(&cimi, &newVid, &err);
1559
- if(newVid!=0){
1560
- zNewUuid = rid_to_uuid(newVid);
1561
- CX("<h3>Manifest%s: %S</h3><pre>"
1562
- "<code class='fileedit-manifest'>%h</code>"
1563
- "</pre>",
1564
- (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
1565
- zNewUuid, blob_str(&manifest));
1566
- if(!(CIMINI_DRY_RUN & cimi.flags)){
1567
- /* We need to update certain form fields and UI elements so
1568
- ** they're not left pointing to the previous version. While
1569
- ** we're at it, we'll re-enable dry-run mode for sanity's
1570
- ** sake.
1571
- */
1572
- blob_appendf(&endScript,
1573
- "/* Toggle dry-run back on */\n"
1574
- "document.querySelector('input[type=checkbox]"
1575
- "[name=dry_run]').checked=true;\n");
1576
- blob_appendf(&endScript,
1577
- "/* Update version number */\n"
1578
- "document.querySelector('input[name=r]')"
1579
- ".value=%Q;\n"
1580
- "document.querySelector('#r-label')"
1581
- ".innerText=%Q;\n"
1582
- "document.querySelector('#r-link')"
1583
- ".setAttribute('href', '%R/info/%!S');\n"
1584
- "document.querySelector('#finfo-link')"
1585
- ".setAttribute('href','%R/finfo?name=%T&m=%!S');\n",
1586
- /*input[name=r]:*/zNewUuid, /*#r-label:*/ zNewUuid,
1587
- /*#r-link:*/ zNewUuid,
1588
- /*#finfo-link:*/zFilename, zNewUuid);
1589
- blob_appendf(&endScript,
1590
- "/* Updated finfo link */"
1591
- );
1592
- blob_appendf(&endScript,
1593
- "/* Update permalink */\n"
1594
- "const urlFull='%R/fileedit?file=%T&r=%!S';\n"
1595
- "const urlShort='/fileedit?file=%T&r=%!S';\n"
1596
- "let link=document.querySelector('#permalink');\n"
1597
- "link.innerText=urlShort;\n"
1598
- "link.setAttribute('href',urlFull);\n",
1599
- cimi.zFilename, zNewUuid,
1600
- cimi.zFilename, zNewUuid);
1601
- }
1602
- fossil_free(zNewUuid);
1603
- zNewUuid = 0;
1604
- }
1605
- /* On error, the error message is in the err blob and will
1606
- ** be emitted below. */
1607
- cimi.pMfOut = 0;
1608
- blob_reset(&manifest);
1609
- }else if(SUBMIT_PREVIEW==submitMode){
1610
- int pflags = 0;
1611
- if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1612
- fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1613
- previewRenderMode, previewHtmlHeight);
1614
- }else if(SUBMIT_DIFF_SBS==submitMode
1615
- || SUBMIT_DIFF_UNIFIED==submitMode){
1616
- fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1617
- SUBMIT_DIFF_SBS==submitMode);
1618
- }else{
1619
- /* Ignore invalid submitMode value */
1620
- goto end_footer;
1621
- }
1622
-
16231563
end_footer:
16241564
zContent = 0;
16251565
fossil_free(zFileUuid);
16261566
if(stmt.pStmt){
16271567
db_finalize(&stmt);
16281568
}
16291569
if(blob_size(&err)){
1630
- CX("<div class='fileedit-error-report'>%s</div>",
1631
- blob_str(&err));
1570
+ CX("<div class='fileedit-error-report'>%s</div>",
1571
+ blob_str(&err));
1572
+ }else if(blob_size(&submitResult)){
1573
+ CX("%b",&submitResult);
16321574
}
1575
+ blob_reset(&submitResult);
16331576
blob_reset(&err);
16341577
CheckinMiniInfo_cleanup(&cimi);
16351578
if(blob_size(&endScript)>0){
1636
- fileedit_emit_script(0);
1579
+ style_emit_script_tag(0);
16371580
CX("(function(){\n");
16381581
CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
16391582
&endScript);
16401583
CX("})();");
1641
- fileedit_emit_script(1);
1584
+ style_emit_script_tag(1);
16421585
}
16431586
db_end_transaction(0/*noting that dry-run mode will have already
16441587
** set this to rollback mode. */);
16451588
style_footer();
16461589
}
16471590
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,61 +898,97 @@
898 fossil_free(zGlobs);
899 }
900 return glob_match(pGlobs, zFilename);
901 }
902
903 static void fileedit_emit_script(int phase){
904 if(0==phase){
905 CX("<script nonce='%s'>", style_nonce());
906 }else{
907 CX("</script>\n");
908 }
909 }
910
911 /*
912 ** Emits a script tag which defines window.fossilFetch(), which works
913 ** similarly (not identically) to the not-quite-ubiquitous global
914 ** fetch().
915 **
916 ** JS usages:
917 **
918 ** fossilFetch( URI, onLoadCallback );
919 **
920 ** fossilFetch( URI, optionsObject );
921 **
922 ** Where the optionsObject may be an object with any of these
923 ** properties:
 
 
 
 
924 **
925 ** - onload: callback(responseData) (default = output response to
926 ** console).
927 **
928 ** - onerror: callback(XHR onload event) (default = no-op)
929 **
930 ** - method: 'POST' | 'GET' (default = 'GET')
931 **
932 ** Noting that URI must be relative to the top of the repository and
933 ** must not start with a slash. It gets %R/ prepended to it.
934 **
935 ** TODOs, if needed, include:
936 **
937 ** optionsObject.params: object map of key/value pairs to append to the
938 ** URI.
939 **
940 ** optionsObject.payload: string or JSON-able object to POST as the
941 ** payload.
 
 
 
 
 
 
 
 
 
942 **
943 */
944 static void fileedit_emit_script_fetch(){
945 fileedit_emit_script(0);
946 CX("window.fossilFetch = function(path,opt){\n");
947 CX(" if('function'===typeof opt){\n");
948 CX(" opt={onload:opt};\n");
949 CX(" }else{\n");
950 CX(" opt=opt||{onload:function(r){console.debug('response:',r)}}\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
951 CX(" }\n");
952 CX(" const url='%R/'+path, x=new XMLHttpRequest();\n");
953 CX(" x.open(opt.method||'GET', url, true);\n");
954 CX(" x.responseType=opt.responseType||'text';\n");
955 CX(" if(opt.onload){\n");
956 CX(" x.onload = function(e){\n");
957 CX(" if(200!==this.status){\n");
958 CX(" if(opt.onerror) opt.onerror(e);\n");
@@ -959,40 +995,16 @@
959 CX(" return;\n");
960 CX(" }\n");
961 CX(" opt.onload(this.response);\n");
962 CX(" }\n");
963 CX(" }\n");
964 CX(" x.send();");
 
965 CX("};\n");
966 fileedit_emit_script(1);
967 };
968
969 /*
970 ** Outputs a labeled checkbox element:
971 **
972 ** <span class='input-with-label' title={{zTip}}>
973 ** <input type='checkbox' name={{zFieldName}} value={{zValue}}
974 ** {{isChecked ? " checked : ""}}/>
975 ** <span>{{zLabel}}</span>
976 ** </span>
977 **
978 ** zFieldName, zLabel, and zValue are required. zTip is optional.
979 */
980 static void style_labeled_checkbox(const char *zFieldName,
981 const char * zLabel,
982 const char * zValue,
983 const char * zTip,
984 int isChecked){
985 CX("<div class='input-with-label'");
986 if(zTip && *zTip){
987 CX(" title='%h'", zTip);
988 }
989 CX("><input type='checkbox' name='%s' value='%T'%s/>",
990 zFieldName,
991 zValue ? zValue : "", isChecked ? " checked" : "");
992 CX("<span>%h</span></div>", zLabel);
993 }
994
995 enum fileedit_render_preview_flags {
996 FE_PREVIEW_LINE_NUMBERS = 1
997 };
998 enum fileedit_render_modes {
@@ -1030,15 +1042,16 @@
1030 CX("<div class='fileedit-preview'>");
1031 CX("<div>Preview</div>");
1032 switch(renderMode){
1033 case FE_RENDER_HTML:{
1034 char * z64 = encode64(blob_str(pContent), blob_size(pContent));
1035 CX("<iframe width='100%%' frameborder='0' marginwidth='0' "
1036 "style='height:%dem' "
1037 "marginheight='0' sandbox='allow-same-origin' id='ifm1' "
1038 "src='data:text/html;base64,%z'"
1039 "></iframe>", nIframeHeightEm ? nIframeHeightEm : 40,
 
1040 z64);
1041 break;
1042 }
1043 case FE_RENDER_WIKI:
1044 wiki_render_by_mimetype(pContent, zMime);
@@ -1061,27 +1074,22 @@
1061 }
1062
1063 /*
1064 ** Renders diffs for the /fileedit page. pContent is the
1065 ** locally-edited content. frid is the RID of the file's blob entry
1066 ** from which pContent is based. zManifestUuid is the checkin version
1067 ** to which RID belongs - it is purely informational, for labeling the
1068 ** diff view. isSbs is true for side-by-side diffs, false for unified.
1069 */
1070 static void fileedit_render_diff(Blob * pContent, int frid,
1071 const char * zManifestUuid,
1072 int isSbs){
1073 Blob orig = empty_blob;
1074 Blob out = empty_blob;
1075 u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR;
1076
1077 content_get(frid, &orig);
1078 if(isSbs){
1079 diffFlags |= DIFF_SIDEBYSIDE;
1080 }else{
1081 diffFlags |= DIFF_LINENO;
1082 }
1083 text_diff(&orig, pContent, &out, 0, diffFlags);
1084 CX("<div class='fileedit-diff'>");
1085 CX("<div>Diff <code>[%S]</code> &rarr; Local Edits</div>",
1086 zManifestUuid);
1087 if(isSbs){
@@ -1090,91 +1098,33 @@
1090 CX("<pre class='udiff'>%b</pre>",&out);
1091 }
1092 CX("</div><!--.fileedit-diff-->\n");
1093 blob_reset(&orig);
1094 blob_reset(&out);
1095 /* Wow, that was *easy*. */
1096 }
1097
1098 /*
1099 ** Outputs a SELECT list from a compile-time list of integers.
1100 ** The vargs must be a list of (const char *, int) pairs, terminated
1101 ** with a single NULL. Each pair is interpreted as...
1102 **
1103 ** If the (const char *) is NULL, it is the end of the list, else
1104 ** a new OPTION entry is created. If the string is empty, the
1105 ** label and value of the OPTION is the integer part of the pair.
1106 ** If the string is not empty, it becomes the label and the integer
1107 ** the value. If that value == selectedValue then that OPTION
1108 ** element gets the 'selected' attribute.
1109 **
1110 ** Note that the pairs are not in (int, const char *) order because
1111 ** there is no well-known integer value which we can definitively use
1112 ** as a list terminator.
1113 **
1114 ** zFieldName is the value of the form element's name attribute.
1115 **
1116 ** zLabel is an optional string to use as a "label" for the element
1117 ** (see below).
1118 **
1119 ** zTooltip is an optional value for the SELECT's title attribute.
1120 **
1121 ** The structure of the emited HTML is:
1122 **
1123 ** <div class='input-with-label'>
1124 ** <span>{{zLabel}}</span>
1125 ** <select>...</select>
1126 ** </div>
1127 **
1128 */
1129 static void style_select_list_int_v(const char *zFieldName,
1130 const char * zLabel,
1131 const char * zToolTip,
1132 int selectedVal, va_list vargs){
1133 CX("<div class='input-with-label'");
1134 if(zToolTip && *zToolTip){
1135 CX(" title='%h'",zToolTip);
1136 }
1137 CX(">");
1138 if(zLabel && *zLabel){
1139 CX("<span>%h</span>", zLabel);
1140 }
1141 CX("<select name='%s'>",zFieldName);
1142 while(1){
1143 const char * zOption = va_arg(vargs,char *);
1144 int v;
1145 if(NULL==zOption){
1146 break;
1147 }
1148 v = va_arg(vargs,int);
1149 CX("<option value='%d'%s>",
1150 v, v==selectedVal ? " selected" : "");
1151 if(*zOption){
1152 CX("%s", zOption);
1153 }else{
1154 CX("%d",v);
1155 }
1156 CX("</option>\n");
1157 }
1158 CX("</select>\n");
1159 if(zLabel && *zLabel){
1160 CX("</div>\n");
1161 }
1162 }
1163
1164 /*
1165 ** The ellipsis-args counterpart of style_select_list_int_v().
1166 */
1167 void style_select_list_int(const char *zFieldName,
1168 const char * zLabel,
1169 const char * zToolTip,
1170 int selectedVal, ... ){
1171 va_list vargs;
1172 va_start(vargs,selectedVal);
1173 style_select_list_int_v(zFieldName, zLabel, zToolTip,
1174 selectedVal, vargs);
1175 va_end(vargs);
1176 }
1177
1178 /*
1179 ** WEBPAGE: fileedit
1180 **
@@ -1212,10 +1162,11 @@
1212 int previewLn = P("preview_ln")!=0; /* Line number mode */
1213 int previewHtmlHeight = 0; /* iframe height (EMs) */
1214 int previewRenderMode = FE_RENDER_GUESS; /* preview mode */
1215 char * zFileUuid = 0; /* File content UUID */
1216 Blob err = empty_blob; /* Error report */
 
1217 const char * zFlagCheck = 0; /* Temp url flag holder */
1218 Blob endScript = empty_blob; /* Script code to run at the
1219 end. This content will be
1220 combined into a single JS
1221 function call, thus each
@@ -1285,27 +1236,19 @@
1285 cimi.zFilename = mprintf("%s",zFilename);
1286 zFileMime = mimetype_from_name(zFilename);
1287
1288 /* Find the repo-side file entry or fail... */
1289 cimi.zParentUuid = rid_to_uuid(vid);
1290 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1291 "WHERE filename=%Q %s AND checkinID=%d",
1292 zFilename, filename_collation(), vid);
1293 if(SQLITE_ROW==db_step(&stmt)){
1294 const char * zPerm = db_column_text(&stmt, 1);
1295 cimi.filePerm = mfile_permstr_int(zPerm);
1296 if(PERM_LNK==cimi.filePerm){
1297 fail((&err,"Editing symlinks is not permitted."));
1298 }
1299 zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
1300 }
1301 db_finalize(&stmt);
1302 if(!zFileUuid){
1303 fail((&err,"Checkin [%S] does not contain file: "
1304 "<code>%h</code>",
1305 cimi.zParentUuid, zFilename));
1306 }
 
 
 
1307 frid = fast_uuid_to_rid(zFileUuid);
1308 assert(frid);
1309
1310 /* Read file content from submit request or repo... */
1311 if(zContent==0){
@@ -1318,11 +1261,100 @@
1318 if(looks_like_binary(&cimi.fileContent)){
1319 fail((&err,"File appears to be binary. Cannot edit: "
1320 "<code>%h</code>",zFilename));
1321 }
1322
1323 /* All set. Here we go... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
1325 CX("<h1>Editing:</h1>");
1326 CX("<p class='fileedit-hint'>");
1327 CX("File: "
1328 "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
@@ -1338,12 +1370,12 @@
1338 "(Clicking the permalink will reload the page and discard "
1339 "all edits!)",
1340 zFilename, cimi.zParentUuid,
1341 zFilename, cimi.zParentUuid);
1342 CX("</p>");
1343 CX("<p>This page is <em>far from complete</em> and may still have "
1344 "significant bugs. USE AT YOUR OWN RISK, preferably on a test "
1345 "repo.</p>\n");
1346
1347 CX("<form action='%R/fileedit#options' method='POST' "
1348 "class='fileedit'>\n");
1349
@@ -1368,78 +1400,49 @@
1368 CX("<fieldset class='fileedit-options' id='options'>"
1369 "<legend>Options</legend><div>"
1370 /* Chrome does not sanely lay out multiple
1371 ** fieldset children after the <legend>, so
1372 ** a containing div is necessary. */);
1373 /*
1374 ** TODO?: date-override date selection field. Maybe use
1375 ** an input[type=datetime-local].
1376 */
1377 if(SUBMIT_NONE==submitMode || P("dry_run")!=0){
1378 cimi.flags |= CIMINI_DRY_RUN;
1379 }
1380 style_labeled_checkbox("dry_run", "Dry-run?", "1",
1381 "In dry-run mode, the Save button performs "
1382 "all work needed for saving but then rolls "
1383 "back the transaction, and thus does not "
1384 "really save.",
1385 cimi.flags & CIMINI_DRY_RUN);
1386 if(P("allow_fork")!=0){
1387 cimi.flags |= CIMINI_ALLOW_FORK;
1388 }
1389 style_labeled_checkbox("allow_fork", "Allow fork?", "1",
1390 "Allow saving to create a fork?",
1391 cimi.flags & CIMINI_ALLOW_FORK);
1392 if(P("allow_older")!=0){
1393 cimi.flags |= CIMINI_ALLOW_OLDER;
1394 }
1395 style_labeled_checkbox("allow_older", "Allow older?", "1",
1396 "Allow saving against a parent version "
1397 "which has a newer timestamp?",
1398 cimi.flags & CIMINI_ALLOW_OLDER);
1399 if(P("exec_bit")!=0){
1400 cimi.filePerm = PERM_EXE;
1401 }
1402 style_labeled_checkbox("exec_bit", "Executable?", "1",
1403 "Set the executable bit?",
1404 PERM_EXE==cimi.filePerm);
1405 if(P("allow_merge_conflict")!=0){
1406 cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
1407 }
1408 style_labeled_checkbox("allow_merge_conflict",
1409 "Allow merge conflict markers?", "1",
1410 "Allow saving even if the content contains "
1411 "what appear to be fossil merge conflict "
1412 "markers?",
1413 cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
1414 if(P("prefer_delta")!=0){
1415 cimi.flags |= CIMINI_PREFER_DELTA;
1416 }
1417 style_labeled_checkbox("prefer_delta",
1418 "Prefer delta manifest?", "1",
1419 "Will create a delta manifest, instead of "
1420 "baseline, if conditions are favorable to do "
1421 "so. This option is only a suggestion.",
1422 cimi.flags & CIMINI_PREFER_DELTA);
1423 {/* EOL conversion policy... */
1424 const int eolMode = submitMode==SUBMIT_NONE
1425 ? 0 : atoi(PD("eol","0"));
1426 switch(eolMode){
1427 case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
1428 case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
1429 default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
1430 }
1431 style_select_list_int("eol", "EOL Style",
1432 "EOL conversion policy, noting that "
1433 "form-processing may implicitly change the "
1434 "line endings of the input.",
1435 eolMode==1||eolMode==2 ? eolMode : 0,
1436 "Inherit", 0,
1437 "Unix", 1,
1438 "Windows", 2,
1439 NULL);
1440 }
1441
1442 CX("</div></fieldset>") /* end of checkboxes */;
1443
1444 /******* Comment *******/
1445 CX("<a id='comment'></a>");
@@ -1446,25 +1449,23 @@
1446 CX("<fieldset><legend>Commit message</legend><div>");
1447 CX("<textarea name='comment' rows='3' cols='80'>");
1448 /* ^^^ adding the 'required' attribute means we cannot even submit
1449 ** for PREVIEW mode if it's empty :/. */
1450 if(zComment && *zComment){
1451 CX("%h"/*%h? %s?*/, zComment);
1452 }
1453 CX("</textarea>\n");
1454 CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup "
1455 "syntax.</div>\n"/*TODO: select for fossil/md/plain text*/);
1456 CX("</div></fieldset>\n");
1457
1458
1459
1460 /******* Buttons *******/
1461 CX("<a id='buttons'></a>");
1462 CX("<fieldset class='fileedit-options'>"
1463 "<legend>Tell the server to...</legend><div>");
1464 CX("<button type='submit' name='submit' value='%d'>"
1465 "Save</button>", SUBMIT_SAVE);
1466 CX("<button type='submit' name='submit' value='%d'>"
1467 "Preview</button>", SUBMIT_PREVIEW);
1468 {
1469 /* Preview rendering mode selection... */
1470 previewRenderMode = atoi(PD("preview_render_mode","0"));
@@ -1512,12 +1513,29 @@
1512 "Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED);
1513 CX("</div></fieldset>");
1514
1515 /******* End of form *******/
1516 CX("</form>\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
1518 /* Dynamically populate the editor... */
 
1519 if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){
1520 char const * zQuoted = 0;
1521 if(blob_size(&cimi.fileContent)>0){
1522 db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1523 db_step(&stmt);
@@ -1530,117 +1548,42 @@
1530 if(stmt.pStmt){
1531 db_finalize(&stmt);
1532 }
1533 }else if(2==loadMode){
1534 assert(submitMode==SUBMIT_NONE);
1535 fileedit_emit_script_fetch();
1536 blob_appendf(&endScript,
1537 "window.fossilFetch('raw/%s',{"
1538 "onload: (r)=>document.getElementById('fileedit-content')"
1539 ".value=r,"
1540 "onerror:()=>document.getElementById('fileedit-content')"
1541 ".value="
1542 "'Error loading content'"
1543 "});\n", zFileUuid);
1544 }
1545
1546 if(SUBMIT_SAVE==submitMode){
1547 Blob manifest = empty_blob;
1548 char * zNewUuid = 0;
1549 /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
1550 if(zComment && *zComment){
1551 blob_append(&cimi.comment, zComment, -1);
1552 }else{
1553 fail((&err,"Empty comment is not permitted."));
1554 }
1555 /*cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
1556 assert(cimi.pParent && "We know vid is valid.");*/
1557 cimi.pMfOut = &manifest;
1558 checkin_mini(&cimi, &newVid, &err);
1559 if(newVid!=0){
1560 zNewUuid = rid_to_uuid(newVid);
1561 CX("<h3>Manifest%s: %S</h3><pre>"
1562 "<code class='fileedit-manifest'>%h</code>"
1563 "</pre>",
1564 (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
1565 zNewUuid, blob_str(&manifest));
1566 if(!(CIMINI_DRY_RUN & cimi.flags)){
1567 /* We need to update certain form fields and UI elements so
1568 ** they're not left pointing to the previous version. While
1569 ** we're at it, we'll re-enable dry-run mode for sanity's
1570 ** sake.
1571 */
1572 blob_appendf(&endScript,
1573 "/* Toggle dry-run back on */\n"
1574 "document.querySelector('input[type=checkbox]"
1575 "[name=dry_run]').checked=true;\n");
1576 blob_appendf(&endScript,
1577 "/* Update version number */\n"
1578 "document.querySelector('input[name=r]')"
1579 ".value=%Q;\n"
1580 "document.querySelector('#r-label')"
1581 ".innerText=%Q;\n"
1582 "document.querySelector('#r-link')"
1583 ".setAttribute('href', '%R/info/%!S');\n"
1584 "document.querySelector('#finfo-link')"
1585 ".setAttribute('href','%R/finfo?name=%T&m=%!S');\n",
1586 /*input[name=r]:*/zNewUuid, /*#r-label:*/ zNewUuid,
1587 /*#r-link:*/ zNewUuid,
1588 /*#finfo-link:*/zFilename, zNewUuid);
1589 blob_appendf(&endScript,
1590 "/* Updated finfo link */"
1591 );
1592 blob_appendf(&endScript,
1593 "/* Update permalink */\n"
1594 "const urlFull='%R/fileedit?file=%T&r=%!S';\n"
1595 "const urlShort='/fileedit?file=%T&r=%!S';\n"
1596 "let link=document.querySelector('#permalink');\n"
1597 "link.innerText=urlShort;\n"
1598 "link.setAttribute('href',urlFull);\n",
1599 cimi.zFilename, zNewUuid,
1600 cimi.zFilename, zNewUuid);
1601 }
1602 fossil_free(zNewUuid);
1603 zNewUuid = 0;
1604 }
1605 /* On error, the error message is in the err blob and will
1606 ** be emitted below. */
1607 cimi.pMfOut = 0;
1608 blob_reset(&manifest);
1609 }else if(SUBMIT_PREVIEW==submitMode){
1610 int pflags = 0;
1611 if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1612 fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1613 previewRenderMode, previewHtmlHeight);
1614 }else if(SUBMIT_DIFF_SBS==submitMode
1615 || SUBMIT_DIFF_UNIFIED==submitMode){
1616 fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1617 SUBMIT_DIFF_SBS==submitMode);
1618 }else{
1619 /* Ignore invalid submitMode value */
1620 goto end_footer;
1621 }
1622
1623 end_footer:
1624 zContent = 0;
1625 fossil_free(zFileUuid);
1626 if(stmt.pStmt){
1627 db_finalize(&stmt);
1628 }
1629 if(blob_size(&err)){
1630 CX("<div class='fileedit-error-report'>%s</div>",
1631 blob_str(&err));
 
 
1632 }
 
1633 blob_reset(&err);
1634 CheckinMiniInfo_cleanup(&cimi);
1635 if(blob_size(&endScript)>0){
1636 fileedit_emit_script(0);
1637 CX("(function(){\n");
1638 CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
1639 &endScript);
1640 CX("})();");
1641 fileedit_emit_script(1);
1642 }
1643 db_end_transaction(0/*noting that dry-run mode will have already
1644 ** set this to rollback mode. */);
1645 style_footer();
1646 }
1647
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,61 +898,97 @@
898 fossil_free(zGlobs);
899 }
900 return glob_match(pGlobs, zFilename);
901 }
902
 
 
 
 
 
 
 
 
903 /*
904 ** Emits a script tag which defines window.fossil.fetch(), which works
905 ** similarly (not identically) to the not-quite-ubiquitous global
906 ** fetch().
907 **
908 ** JS usages:
909 **
910 ** fossilFetch( URI, onLoadCallback );
911 **
912 ** fossilFetch( URI, optionsObject );
913 **
914 ** Noting that URI must be relative to the top of the repository and
915 ** must not start with a slash (if it does, it is stripped). It gets
916 ** %R/ prepended to it.
917 **
918 ** The optionsObject may be an onload callback or an object with any
919 ** of these properties:
920 **
921 ** - onload: callback(responseData) (default = output response to
922 ** console).
923 **
924 ** - onerror: callback(XHR onload event) (default = console output)
925 **
926 ** - method: 'POST' | 'GET' (default = 'GET')
927 **
928 ** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
929 ** Document, FormData, Blob, File, ArrayBuffer), or a plain object
930 ** or array, either of which gets JSON.stringify()'d. If set then
931 ** the method is automatically set to 'POST'. If an object/array is
932 ** converted to JSON, the content-type is set to 'application/json'.
933 ** By default XHR2 will set the content type based on the payload
934 ** type.
935 **
936 ** - contentType: Optional request content type when POSTing. Ignored
937 ** if the method is not 'POST'.
938 **
939 ** - responseType: optional string. One of ("text", "arraybuffer",
940 ** "blob", or "document") (as specified by XHR2). Default = "text".
941 **
942 ** - urlParams: string|object. If a string, it is assumed to be a
943 ** URI-encoded list of params in the form "key1=val1&key2=val2...",
944 ** with NO leading '?'. If it is an object, all of its properties
945 ** get converted to that form. Either way, the parameters get
946 ** appended to the URL.
947 **
948 */
949 void fileedit_emit_script_fetch(){
950 style_emit_script_tag(0);
951 CX("fossil.fetch = function(path,opt){\n");
952 CX(" if('/'===path[0]) path = path.substr(1);\n");
953 CX(" if(!opt){\n");
954 CX(" opt = {onload:(r)=>console.debug('response:',r)};\n");
955 CX(" }else if('function'===typeof opt){\n");
956 CX(" opt={onload:opt,\n");
957 CX(" onerror:(e)=>console.error('ajax error:',e)};\n");
958 CX(" }\n");
959 CX(" let payload = opt.payload;\n");
960 CX(" if(payload){\n");
961 CX(" opt.method = 'POST';\n");
962 CX(" if(!(payload instanceof FormData)\n");
963 CX(" && !(payload instanceof Document)\n");
964 CX(" && !(payload instanceof Blob)\n");
965 CX(" && !(payload instanceof File)\n");
966 CX(" && !(payload instanceof ArrayBuffer)){\n");
967 CX(" if('object'===typeof payload || payload instanceof Array){\n");
968 CX(" payload = JSON.stringify(payload);\n");
969 CX(" opt.contentType = 'application/json';\n");
970 CX(" }\n");
971 CX(" }\n");
972 CX(" }\n");
973 CX(" const url=['%R/'+path], x=new XMLHttpRequest();\n");
974 CX(" if(opt.urlParams){\n");
975 CX(" url.push('?');\n");
976 CX(" if('string'===typeof opt.urlParams){\n");
977 CX(" url.push(opt.urlParams);\n");
978 CX(" }else{/*assume object*/\n");
979 CX(" let k, i = 0;\n");
980 CX(" for( k in opt.urlParams ){\n");
981 CX(" if(i++) url.push('&');\n");
982 CX(" url.push(k,'=',encodeURIComponent(opt.urlParams[k]));\n");
983 CX(" }\n");
984 CX(" }\n");
985 CX(" }\n");
986 CX(" if('POST'===opt.method && 'string'===typeof opt.contentType){\n");
987 CX(" x.setRequestHeader('Content-Type',opt.contentType);\n");
988 CX(" }\n");
989 CX(" x.open(opt.method||'GET', url.join(''), true);\n");
 
990 CX(" x.responseType=opt.responseType||'text';\n");
991 CX(" if(opt.onload){\n");
992 CX(" x.onload = function(e){\n");
993 CX(" if(200!==this.status){\n");
994 CX(" if(opt.onerror) opt.onerror(e);\n");
@@ -959,40 +995,16 @@
995 CX(" return;\n");
996 CX(" }\n");
997 CX(" opt.onload(this.response);\n");
998 CX(" }\n");
999 CX(" }\n");
1000 CX(" if(payload) x.send(payload);\n");
1001 CX(" else x.send();\n");
1002 CX("};\n");
1003 style_emit_script_tag(1);
1004 };
1005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
1007 enum fileedit_render_preview_flags {
1008 FE_PREVIEW_LINE_NUMBERS = 1
1009 };
1010 enum fileedit_render_modes {
@@ -1030,15 +1042,16 @@
1042 CX("<div class='fileedit-preview'>");
1043 CX("<div>Preview</div>");
1044 switch(renderMode){
1045 case FE_RENDER_HTML:{
1046 char * z64 = encode64(blob_str(pContent), blob_size(pContent));
1047 CX("<iframe width='100%%' frameborder='0' "
1048 "marginwidth='0' style='height:%dem' "
1049 "marginheight='0' sandbox='allow-same-origin' "
1050 "id='ifm1' src='data:text/html;base64,%z'"
1051 "></iframe>",
1052 nIframeHeightEm ? nIframeHeightEm : 40,
1053 z64);
1054 break;
1055 }
1056 case FE_RENDER_WIKI:
1057 wiki_render_by_mimetype(pContent, zMime);
@@ -1061,27 +1074,22 @@
1074 }
1075
1076 /*
1077 ** Renders diffs for the /fileedit page. pContent is the
1078 ** locally-edited content. frid is the RID of the file's blob entry
1079 ** from which pContent is based. zManifestUuid is the checkin version
1080 ** to which RID belongs - it is purely informational, for labeling the
1081 ** diff view. isSbs is true for side-by-side diffs, false for unified.
1082 */
1083 static void fileedit_render_diff(Blob * pContent, int frid,
1084 const char * zManifestUuid,
1085 int isSbs){
1086 Blob orig = empty_blob;
1087 Blob out = empty_blob;
1088 u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
1089 | (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
1090 content_get(frid, &orig);
 
 
 
 
 
1091 text_diff(&orig, pContent, &out, 0, diffFlags);
1092 CX("<div class='fileedit-diff'>");
1093 CX("<div>Diff <code>[%S]</code> &rarr; Local Edits</div>",
1094 zManifestUuid);
1095 if(isSbs){
@@ -1090,91 +1098,33 @@
1098 CX("<pre class='udiff'>%b</pre>",&out);
1099 }
1100 CX("</div><!--.fileedit-diff-->\n");
1101 blob_reset(&orig);
1102 blob_reset(&out);
1103 }
1104
1105 /*
1106 ** Given a repo-relative filename and a manifest RID, returns the UUID
1107 ** of the corresponding file entry. Returns NULL if no match is
1108 ** found. If pFilePerm is not NULL, the file's permission flag value
1109 ** is written to *pFilePerm.
1110 */
1111 static char *fileedit_file_uuid(char const *zFilename,
1112 int vid, int *pFilePerm){
1113 Stmt stmt = empty_Stmt;
1114 char * zFileUuid = 0;
1115 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
1116 "WHERE filename=%Q %s AND checkinID=%d",
1117 zFilename, filename_collation(), vid);
1118 if(SQLITE_ROW==db_step(&stmt)){
1119 zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
1120 if(pFilePerm){
1121 *pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1));
1122 }
1123 }
1124 db_finalize(&stmt);
1125 return zFileUuid;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126 }
1127
1128 /*
1129 ** WEBPAGE: fileedit
1130 **
@@ -1212,10 +1162,11 @@
1162 int previewLn = P("preview_ln")!=0; /* Line number mode */
1163 int previewHtmlHeight = 0; /* iframe height (EMs) */
1164 int previewRenderMode = FE_RENDER_GUESS; /* preview mode */
1165 char * zFileUuid = 0; /* File content UUID */
1166 Blob err = empty_blob; /* Error report */
1167 Blob submitResult = empty_blob; /* Error report */
1168 const char * zFlagCheck = 0; /* Temp url flag holder */
1169 Blob endScript = empty_blob; /* Script code to run at the
1170 end. This content will be
1171 combined into a single JS
1172 function call, thus each
@@ -1285,27 +1236,19 @@
1236 cimi.zFilename = mprintf("%s",zFilename);
1237 zFileMime = mimetype_from_name(zFilename);
1238
1239 /* Find the repo-side file entry or fail... */
1240 cimi.zParentUuid = rid_to_uuid(vid);
1241 zFileUuid = fileedit_file_uuid(zFilename, vid, &cimi.filePerm);
 
 
 
 
 
 
 
 
 
 
 
1242 if(!zFileUuid){
1243 fail((&err,"Checkin [%S] does not contain file: "
1244 "<code>%h</code>",
1245 cimi.zParentUuid, zFilename));
1246 }
1247 else if(PERM_LNK==cimi.filePerm){
1248 fail((&err,"Editing symlinks is not permitted."));
1249 }
1250 frid = fast_uuid_to_rid(zFileUuid);
1251 assert(frid);
1252
1253 /* Read file content from submit request or repo... */
1254 if(zContent==0){
@@ -1318,11 +1261,100 @@
1261 if(looks_like_binary(&cimi.fileContent)){
1262 fail((&err,"File appears to be binary. Cannot edit: "
1263 "<code>%h</code>",zFilename));
1264 }
1265
1266 /*
1267 ** TODO?: date-override date selection field. Maybe use
1268 ** an input[type=datetime-local].
1269 */
1270 if(SUBMIT_NONE==submitMode || P("dry_run")!=0){
1271 cimi.flags |= CIMINI_DRY_RUN;
1272 }
1273 if(P("allow_fork")!=0){
1274 cimi.flags |= CIMINI_ALLOW_FORK;
1275 }
1276 if(P("allow_older")!=0){
1277 cimi.flags |= CIMINI_ALLOW_OLDER;
1278 }
1279 if(P("exec_bit")!=0){
1280 cimi.filePerm = PERM_EXE;
1281 }
1282 if(P("allow_merge_conflict")!=0){
1283 cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
1284 }
1285 if(P("prefer_delta")!=0){
1286 cimi.flags |= CIMINI_PREFER_DELTA;
1287 }
1288 /* EOL conversion policy... */
1289 {
1290 const int eolMode = submitMode==SUBMIT_NONE
1291 ? 0 : atoi(PD("eol","0"));
1292 switch(eolMode){
1293 case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
1294 case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
1295 default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
1296 }
1297 }
1298
1299 /********************************************************************
1300 ** All errors which "could" have happened up to this point are of a
1301 ** degree which keep us from rendering the rest of the page, and
1302 ** thus fail() has already skipped to the end of the page to render
1303 ** the errors. Any up-coming errors, barring malloc failure or
1304 ** similar, are not "that" fatal. We can/should continue rendering
1305 ** the page, then output the error message at the end.
1306 **
1307 ** Because we cannot intercept the output of the PREVIEW and DIFF
1308 ** rendering, we have to delay the "real work" for those modes until
1309 ** after the rest of the page has been rendered. In the case of
1310 ** SAVE, we can capture all of the output, and thus can perform that
1311 ** work before rendering, which is important so that we have the
1312 ** proper version information when rendering the rest of the page.
1313 ********************************************************************/
1314 #undef fail
1315 while(SUBMIT_SAVE==submitMode){
1316 Blob manifest = empty_blob;
1317 /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
1318 if(zComment && *zComment){
1319 blob_append(&cimi.comment, zComment, -1);
1320 }else{
1321 blob_append(&err,"Empty checkin comment is not permitted.",-1);
1322 break;
1323 }
1324 cimi.pMfOut = &manifest;
1325 checkin_mini(&cimi, &newVid, &err);
1326 if(newVid!=0){
1327 char * zNewUuid = rid_to_uuid(newVid);
1328 blob_appendf(&submitResult,
1329 "<h3>Manifest%s: %S</h3><pre>"
1330 "<code class='fileedit-manifest'>%h</code>"
1331 "</pre>",
1332 (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
1333 zNewUuid, blob_str(&manifest));
1334 if(CIMINI_DRY_RUN & cimi.flags){
1335 fossil_free(zNewUuid);
1336 }else{
1337 /* Update cimi version info... */
1338 assert(cimi.pParent);
1339 assert(cimi.zParentUuid);
1340 fossil_free(zFileUuid);
1341 zFileUuid = fileedit_file_uuid(cimi.zFilename, newVid, 0);
1342 manifest_destroy(cimi.pParent);
1343 cimi.pParent = 0;
1344 fossil_free(cimi.zParentUuid);
1345 cimi.zParentUuid = zNewUuid;
1346 zComment = 0;
1347 cimi.flags |= CIMINI_DRY_RUN /* for sanity's sake */;
1348 }
1349 }
1350 /* On error, the error message is in the err blob and will
1351 ** be emitted at the end. */
1352 cimi.pMfOut = 0;
1353 blob_reset(&manifest);
1354 break;
1355 }
1356
1357 CX("<h1>Editing:</h1>");
1358 CX("<p class='fileedit-hint'>");
1359 CX("File: "
1360 "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
@@ -1338,12 +1370,12 @@
1370 "(Clicking the permalink will reload the page and discard "
1371 "all edits!)",
1372 zFilename, cimi.zParentUuid,
1373 zFilename, cimi.zParentUuid);
1374 CX("</p>");
1375 CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
1376 "USE AT YOUR OWN RISK, preferably on a test "
1377 "repo.</p>\n");
1378
1379 CX("<form action='%R/fileedit#options' method='POST' "
1380 "class='fileedit'>\n");
1381
@@ -1368,78 +1400,49 @@
1400 CX("<fieldset class='fileedit-options' id='options'>"
1401 "<legend>Options</legend><div>"
1402 /* Chrome does not sanely lay out multiple
1403 ** fieldset children after the <legend>, so
1404 ** a containing div is necessary. */);
 
 
 
 
 
 
 
1405 style_labeled_checkbox("dry_run", "Dry-run?", "1",
1406 "In dry-run mode, the Save button performs "
1407 "all work needed for saving but then rolls "
1408 "back the transaction, and thus does not "
1409 "really save.",
1410 cimi.flags & CIMINI_DRY_RUN);
 
 
 
1411 style_labeled_checkbox("allow_fork", "Allow fork?", "1",
1412 "Allow saving to create a fork?",
1413 cimi.flags & CIMINI_ALLOW_FORK);
 
 
 
1414 style_labeled_checkbox("allow_older", "Allow older?", "1",
1415 "Allow saving against a parent version "
1416 "which has a newer timestamp?",
1417 cimi.flags & CIMINI_ALLOW_OLDER);
 
 
 
1418 style_labeled_checkbox("exec_bit", "Executable?", "1",
1419 "Set the executable bit?",
1420 PERM_EXE==cimi.filePerm);
 
 
 
1421 style_labeled_checkbox("allow_merge_conflict",
1422 "Allow merge conflict markers?", "1",
1423 "Allow saving even if the content contains "
1424 "what appear to be fossil merge conflict "
1425 "markers?",
1426 cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
 
 
 
1427 style_labeled_checkbox("prefer_delta",
1428 "Prefer delta manifest?", "1",
1429 "Will create a delta manifest, instead of "
1430 "baseline, if conditions are favorable to do "
1431 "so. This option is only a suggestion.",
1432 cimi.flags & CIMINI_PREFER_DELTA);
1433 style_select_list_int("eol", "EOL Style",
1434 "EOL conversion policy, noting that "
1435 "form-processing may implicitly change the "
1436 "line endings of the input.",
1437 (cimi.flags & CIMINI_CONVERT_EOL_UNIX)
1438 ? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS
1439 ? 2 : 0),
1440 "Inherit", 0,
1441 "Unix", 1,
1442 "Windows", 2,
1443 NULL);
 
 
 
 
 
 
 
1444
1445 CX("</div></fieldset>") /* end of checkboxes */;
1446
1447 /******* Comment *******/
1448 CX("<a id='comment'></a>");
@@ -1446,25 +1449,23 @@
1449 CX("<fieldset><legend>Commit message</legend><div>");
1450 CX("<textarea name='comment' rows='3' cols='80'>");
1451 /* ^^^ adding the 'required' attribute means we cannot even submit
1452 ** for PREVIEW mode if it's empty :/. */
1453 if(zComment && *zComment){
1454 CX("%h", zComment);
1455 }
1456 CX("</textarea>\n");
1457 CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup "
1458 "syntax.</div>\n"/*TODO: select for fossil/md/plain text*/);
1459 CX("</div></fieldset>\n");
1460
 
 
1461 /******* Buttons *******/
1462 CX("<a id='buttons'></a>");
1463 CX("<fieldset class='fileedit-options'>"
1464 "<legend>Tell the server to...</legend><div>");
1465 CX("<button type='submit' name='submit' value='%d'>"
1466 "Commit</button>", SUBMIT_SAVE);
1467 CX("<button type='submit' name='submit' value='%d'>"
1468 "Preview</button>", SUBMIT_PREVIEW);
1469 {
1470 /* Preview rendering mode selection... */
1471 previewRenderMode = atoi(PD("preview_render_mode","0"));
@@ -1512,12 +1513,29 @@
1513 "Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED);
1514 CX("</div></fieldset>");
1515
1516 /******* End of form *******/
1517 CX("</form>\n");
1518
1519 /*
1520 ** We cannot intercept the output for PREVIEW
1521 ** and DIFF modes, and therefore have to render those
1522 ** last.
1523 */
1524 if(SUBMIT_PREVIEW==submitMode){
1525 int pflags = 0;
1526 if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1527 fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1528 previewRenderMode, previewHtmlHeight);
1529 }else if(SUBMIT_DIFF_SBS==submitMode
1530 || SUBMIT_DIFF_UNIFIED==submitMode){
1531 fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1532 SUBMIT_DIFF_SBS==submitMode);
1533 }
1534
1535 /* Dynamically populate the editor... */
1536 fileedit_emit_script_fetch();
1537 if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){
1538 char const * zQuoted = 0;
1539 if(blob_size(&cimi.fileContent)>0){
1540 db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1541 db_step(&stmt);
@@ -1530,117 +1548,42 @@
1548 if(stmt.pStmt){
1549 db_finalize(&stmt);
1550 }
1551 }else if(2==loadMode){
1552 assert(submitMode==SUBMIT_NONE);
 
1553 blob_appendf(&endScript,
1554 "window.fossil.fetch('raw/%s',{"
1555 "onload: (r)=>document.getElementById('fileedit-content')"
1556 ".value=r,"
1557 "onerror:()=>document.getElementById('fileedit-content')"
1558 ".value="
1559 "'Error loading content'"
1560 "});\n", zFileUuid);
1561 }
1562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1563 end_footer:
1564 zContent = 0;
1565 fossil_free(zFileUuid);
1566 if(stmt.pStmt){
1567 db_finalize(&stmt);
1568 }
1569 if(blob_size(&err)){
1570 CX("<div class='fileedit-error-report'>%s</div>",
1571 blob_str(&err));
1572 }else if(blob_size(&submitResult)){
1573 CX("%b",&submitResult);
1574 }
1575 blob_reset(&submitResult);
1576 blob_reset(&err);
1577 CheckinMiniInfo_cleanup(&cimi);
1578 if(blob_size(&endScript)>0){
1579 style_emit_script_tag(0);
1580 CX("(function(){\n");
1581 CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
1582 &endScript);
1583 CX("})();");
1584 style_emit_script_tag(1);
1585 }
1586 db_end_transaction(0/*noting that dry-run mode will have already
1587 ** set this to rollback mode. */);
1588 style_footer();
1589 }
1590
+134
--- src/style.c
+++ src/style.c
@@ -1295,5 +1295,139 @@
12951295
}
12961296
12971297
#if INTERFACE
12981298
# define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
12991299
#endif
1300
+
1301
+/*
1302
+** Outputs a labeled checkbox element. zFieldName is the form element
1303
+** name. zLabel is the label for the checkbox. zValue is the optional
1304
+** value for the checkbox. zTip is an optional tooltip, which gets set
1305
+** as the "title" attribute of the outermost element. If isChecked is
1306
+** true, the checkbox gets the "checked" attribute set, else it is
1307
+** not.
1308
+**
1309
+** Resulting structure:
1310
+**
1311
+** <div class='input-with-label' title={{zTip}}>
1312
+** <input type='checkbox' name={{zFieldName}} value={{zValue}}
1313
+** {{isChecked ? " checked : ""}}/>
1314
+** <span>{{zLabel}}</span>
1315
+** </div>
1316
+**
1317
+** zFieldName, zLabel, and zValue are required. zTip is optional.
1318
+**
1319
+** Be sure that the input-with-label CSS class is defined sensibly, in
1320
+** particular, having its display:inline-block is useful for alignment
1321
+** purposes.
1322
+*/
1323
+void style_labeled_checkbox(const char *zFieldName, const char * zLabel,
1324
+ const char * zValue, const char * zTip,
1325
+ int isChecked){
1326
+ CX("<div class='input-with-label'");
1327
+ if(zTip && *zTip){
1328
+ CX(" title='%h'", zTip);
1329
+ }
1330
+ CX("><input type='checkbox' name='%s' value='%T'%s/>",
1331
+ zFieldName,
1332
+ zValue ? zValue : "", isChecked ? " checked" : "");
1333
+ CX("<span>%h</span></div>", zLabel);
1334
+}
1335
+
1336
+/*
1337
+** Outputs a SELECT list from a compile-time list of integers.
1338
+** The vargs must be a list of (const char *, int) pairs, terminated
1339
+** with a single NULL. Each pair is interpreted as...
1340
+**
1341
+** If the (const char *) is NULL, it is the end of the list, else
1342
+** a new OPTION entry is created. If the string is empty, the
1343
+** label and value of the OPTION is the integer part of the pair.
1344
+** If the string is not empty, it becomes the label and the integer
1345
+** the value. If that value == selectedValue then that OPTION
1346
+** element gets the 'selected' attribute.
1347
+**
1348
+** Note that the pairs are not in (int, const char *) order because
1349
+** there is no well-known integer value which we can definitively use
1350
+** as a list terminator.
1351
+**
1352
+** zFieldName is the value of the form element's name attribute.
1353
+**
1354
+** zLabel is an optional string to use as a "label" for the element
1355
+** (see below).
1356
+**
1357
+** zTooltip is an optional value for the SELECT's title attribute.
1358
+**
1359
+** The structure of the emitted HTML is:
1360
+**
1361
+** <div class='input-with-label' title={{zToolTip}}>
1362
+** <span>{{zLabel}}</span>
1363
+** <select>...</select>
1364
+** </div>
1365
+**
1366
+** Example:
1367
+**
1368
+** style_select_list_int("my_field", "Grapes",
1369
+** "Select the number of grapes",
1370
+** atoi(PD("my_field","0")),
1371
+** "", 1, "2", 2, "Three", 3,
1372
+** NULL);
1373
+**
1374
+*/
1375
+void style_select_list_int(const char *zFieldName, const char * zLabel,
1376
+ const char * zToolTip, int selectedVal,
1377
+ ... ){
1378
+ va_list vargs;
1379
+ va_start(vargs,selectedVal);
1380
+ CX("<div class='input-with-label'");
1381
+ if(zToolTip && *zToolTip){
1382
+ CX(" title='%h'",zToolTip);
1383
+ }
1384
+ CX(">");
1385
+ if(zLabel && *zLabel){
1386
+ CX("<span>%h</span>", zLabel);
1387
+ }
1388
+ CX("<select name='%s'>",zFieldName);
1389
+ while(1){
1390
+ const char * zOption = va_arg(vargs,char *);
1391
+ int v;
1392
+ if(NULL==zOption){
1393
+ break;
1394
+ }
1395
+ v = va_arg(vargs,int);
1396
+ CX("<option value='%d'%s>",
1397
+ v, v==selectedVal ? " selected" : "");
1398
+ if(*zOption){
1399
+ CX("%s", zOption);
1400
+ }else{
1401
+ CX("%d",v);
1402
+ }
1403
+ CX("</option>\n");
1404
+ }
1405
+ CX("</select>\n");
1406
+ if(zLabel && *zLabel){
1407
+ CX("</div>\n");
1408
+ }
1409
+ va_end(vargs);
1410
+}
1411
+
1412
+
1413
+/*
1414
+** If passed 0, it emits a script opener tag with this session's
1415
+** nonce. If passed non-0 it emits a script closing tag. The very
1416
+** first time it is called, it emits some bootstrapping JS code
1417
+** immediately after the script opener. Specifically, it defines
1418
+** window.fossil if it's not already defined, and may set some
1419
+** properties on it.
1420
+*/
1421
+void style_emit_script_tag(int phase){
1422
+ static int once = 0;
1423
+ if(0==phase){
1424
+ CX("<script nonce='%s'>", style_nonce());
1425
+ if(0==once){
1426
+ once = 1;
1427
+ CX("\nif(!window.fossil) window.fossil={};\n");
1428
+ CX("window.fossil.version = %Q;\n", get_version());
1429
+ }
1430
+ }else{
1431
+ CX("</script>\n");
1432
+ }
1433
+}
13001434
--- src/style.c
+++ src/style.c
@@ -1295,5 +1295,139 @@
1295 }
1296
1297 #if INTERFACE
1298 # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
1299 #endif
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1300
--- src/style.c
+++ src/style.c
@@ -1295,5 +1295,139 @@
1295 }
1296
1297 #if INTERFACE
1298 # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
1299 #endif
1300
1301 /*
1302 ** Outputs a labeled checkbox element. zFieldName is the form element
1303 ** name. zLabel is the label for the checkbox. zValue is the optional
1304 ** value for the checkbox. zTip is an optional tooltip, which gets set
1305 ** as the "title" attribute of the outermost element. If isChecked is
1306 ** true, the checkbox gets the "checked" attribute set, else it is
1307 ** not.
1308 **
1309 ** Resulting structure:
1310 **
1311 ** <div class='input-with-label' title={{zTip}}>
1312 ** <input type='checkbox' name={{zFieldName}} value={{zValue}}
1313 ** {{isChecked ? " checked : ""}}/>
1314 ** <span>{{zLabel}}</span>
1315 ** </div>
1316 **
1317 ** zFieldName, zLabel, and zValue are required. zTip is optional.
1318 **
1319 ** Be sure that the input-with-label CSS class is defined sensibly, in
1320 ** particular, having its display:inline-block is useful for alignment
1321 ** purposes.
1322 */
1323 void style_labeled_checkbox(const char *zFieldName, const char * zLabel,
1324 const char * zValue, const char * zTip,
1325 int isChecked){
1326 CX("<div class='input-with-label'");
1327 if(zTip && *zTip){
1328 CX(" title='%h'", zTip);
1329 }
1330 CX("><input type='checkbox' name='%s' value='%T'%s/>",
1331 zFieldName,
1332 zValue ? zValue : "", isChecked ? " checked" : "");
1333 CX("<span>%h</span></div>", zLabel);
1334 }
1335
1336 /*
1337 ** Outputs a SELECT list from a compile-time list of integers.
1338 ** The vargs must be a list of (const char *, int) pairs, terminated
1339 ** with a single NULL. Each pair is interpreted as...
1340 **
1341 ** If the (const char *) is NULL, it is the end of the list, else
1342 ** a new OPTION entry is created. If the string is empty, the
1343 ** label and value of the OPTION is the integer part of the pair.
1344 ** If the string is not empty, it becomes the label and the integer
1345 ** the value. If that value == selectedValue then that OPTION
1346 ** element gets the 'selected' attribute.
1347 **
1348 ** Note that the pairs are not in (int, const char *) order because
1349 ** there is no well-known integer value which we can definitively use
1350 ** as a list terminator.
1351 **
1352 ** zFieldName is the value of the form element's name attribute.
1353 **
1354 ** zLabel is an optional string to use as a "label" for the element
1355 ** (see below).
1356 **
1357 ** zTooltip is an optional value for the SELECT's title attribute.
1358 **
1359 ** The structure of the emitted HTML is:
1360 **
1361 ** <div class='input-with-label' title={{zToolTip}}>
1362 ** <span>{{zLabel}}</span>
1363 ** <select>...</select>
1364 ** </div>
1365 **
1366 ** Example:
1367 **
1368 ** style_select_list_int("my_field", "Grapes",
1369 ** "Select the number of grapes",
1370 ** atoi(PD("my_field","0")),
1371 ** "", 1, "2", 2, "Three", 3,
1372 ** NULL);
1373 **
1374 */
1375 void style_select_list_int(const char *zFieldName, const char * zLabel,
1376 const char * zToolTip, int selectedVal,
1377 ... ){
1378 va_list vargs;
1379 va_start(vargs,selectedVal);
1380 CX("<div class='input-with-label'");
1381 if(zToolTip && *zToolTip){
1382 CX(" title='%h'",zToolTip);
1383 }
1384 CX(">");
1385 if(zLabel && *zLabel){
1386 CX("<span>%h</span>", zLabel);
1387 }
1388 CX("<select name='%s'>",zFieldName);
1389 while(1){
1390 const char * zOption = va_arg(vargs,char *);
1391 int v;
1392 if(NULL==zOption){
1393 break;
1394 }
1395 v = va_arg(vargs,int);
1396 CX("<option value='%d'%s>",
1397 v, v==selectedVal ? " selected" : "");
1398 if(*zOption){
1399 CX("%s", zOption);
1400 }else{
1401 CX("%d",v);
1402 }
1403 CX("</option>\n");
1404 }
1405 CX("</select>\n");
1406 if(zLabel && *zLabel){
1407 CX("</div>\n");
1408 }
1409 va_end(vargs);
1410 }
1411
1412
1413 /*
1414 ** If passed 0, it emits a script opener tag with this session's
1415 ** nonce. If passed non-0 it emits a script closing tag. The very
1416 ** first time it is called, it emits some bootstrapping JS code
1417 ** immediately after the script opener. Specifically, it defines
1418 ** window.fossil if it's not already defined, and may set some
1419 ** properties on it.
1420 */
1421 void style_emit_script_tag(int phase){
1422 static int once = 0;
1423 if(0==phase){
1424 CX("<script nonce='%s'>", style_nonce());
1425 if(0==once){
1426 once = 1;
1427 CX("\nif(!window.fossil) window.fossil={};\n");
1428 CX("window.fossil.version = %Q;\n", get_version());
1429 }
1430 }else{
1431 CX("</script>\n");
1432 }
1433 }
1434

Keyboard Shortcuts

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