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...
Commit
8d4ce834ed9bd423005ead7281aa36cb212260d45448e1dc3d509784ce02d64f
Parent
fe925e7d6d67b30…
2 files changed
+237
-294
+134
+237
-294
| --- src/fileedit.c | ||
| +++ src/fileedit.c | ||
| @@ -898,61 +898,97 @@ | ||
| 898 | 898 | fossil_free(zGlobs); |
| 899 | 899 | } |
| 900 | 900 | return glob_match(pGlobs, zFilename); |
| 901 | 901 | } |
| 902 | 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 | 903 | /* |
| 912 | -** Emits a script tag which defines window.fossilFetch(), which works | |
| 904 | +** Emits a script tag which defines window.fossil.fetch(), which works | |
| 913 | 905 | ** similarly (not identically) to the not-quite-ubiquitous global |
| 914 | 906 | ** fetch(). |
| 915 | 907 | ** |
| 916 | 908 | ** JS usages: |
| 917 | 909 | ** |
| 918 | 910 | ** fossilFetch( URI, onLoadCallback ); |
| 919 | 911 | ** |
| 920 | 912 | ** fossilFetch( URI, optionsObject ); |
| 921 | 913 | ** |
| 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: | |
| 924 | 920 | ** |
| 925 | 921 | ** - onload: callback(responseData) (default = output response to |
| 926 | 922 | ** console). |
| 927 | 923 | ** |
| 928 | -** - onerror: callback(XHR onload event) (default = no-op) | |
| 924 | +** - onerror: callback(XHR onload event) (default = console output) | |
| 929 | 925 | ** |
| 930 | 926 | ** - method: 'POST' | 'GET' (default = 'GET') |
| 931 | 927 | ** |
| 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. | |
| 942 | 947 | ** |
| 943 | 948 | */ |
| 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"); | |
| 951 | 988 | 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"); | |
| 954 | 990 | CX(" x.responseType=opt.responseType||'text';\n"); |
| 955 | 991 | CX(" if(opt.onload){\n"); |
| 956 | 992 | CX(" x.onload = function(e){\n"); |
| 957 | 993 | CX(" if(200!==this.status){\n"); |
| 958 | 994 | CX(" if(opt.onerror) opt.onerror(e);\n"); |
| @@ -959,40 +995,16 @@ | ||
| 959 | 995 | CX(" return;\n"); |
| 960 | 996 | CX(" }\n"); |
| 961 | 997 | CX(" opt.onload(this.response);\n"); |
| 962 | 998 | CX(" }\n"); |
| 963 | 999 | CX(" }\n"); |
| 964 | - CX(" x.send();"); | |
| 1000 | + CX(" if(payload) x.send(payload);\n"); | |
| 1001 | + CX(" else x.send();\n"); | |
| 965 | 1002 | CX("};\n"); |
| 966 | - fileedit_emit_script(1); | |
| 1003 | + style_emit_script_tag(1); | |
| 967 | 1004 | }; |
| 968 | 1005 | |
| 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 | 1006 | |
| 995 | 1007 | enum fileedit_render_preview_flags { |
| 996 | 1008 | FE_PREVIEW_LINE_NUMBERS = 1 |
| 997 | 1009 | }; |
| 998 | 1010 | enum fileedit_render_modes { |
| @@ -1030,15 +1042,16 @@ | ||
| 1030 | 1042 | CX("<div class='fileedit-preview'>"); |
| 1031 | 1043 | CX("<div>Preview</div>"); |
| 1032 | 1044 | switch(renderMode){ |
| 1033 | 1045 | case FE_RENDER_HTML:{ |
| 1034 | 1046 | 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, | |
| 1040 | 1053 | z64); |
| 1041 | 1054 | break; |
| 1042 | 1055 | } |
| 1043 | 1056 | case FE_RENDER_WIKI: |
| 1044 | 1057 | wiki_render_by_mimetype(pContent, zMime); |
| @@ -1061,27 +1074,22 @@ | ||
| 1061 | 1074 | } |
| 1062 | 1075 | |
| 1063 | 1076 | /* |
| 1064 | 1077 | ** Renders diffs for the /fileedit page. pContent is the |
| 1065 | 1078 | ** 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 | |
| 1067 | 1080 | ** to which RID belongs - it is purely informational, for labeling the |
| 1068 | 1081 | ** diff view. isSbs is true for side-by-side diffs, false for unified. |
| 1069 | 1082 | */ |
| 1070 | 1083 | static void fileedit_render_diff(Blob * pContent, int frid, |
| 1071 | 1084 | const char * zManifestUuid, |
| 1072 | 1085 | int isSbs){ |
| 1073 | 1086 | Blob orig = empty_blob; |
| 1074 | 1087 | 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); | |
| 1077 | 1090 | content_get(frid, &orig); |
| 1078 | - if(isSbs){ | |
| 1079 | - diffFlags |= DIFF_SIDEBYSIDE; | |
| 1080 | - }else{ | |
| 1081 | - diffFlags |= DIFF_LINENO; | |
| 1082 | - } | |
| 1083 | 1091 | text_diff(&orig, pContent, &out, 0, diffFlags); |
| 1084 | 1092 | CX("<div class='fileedit-diff'>"); |
| 1085 | 1093 | CX("<div>Diff <code>[%S]</code> → Local Edits</div>", |
| 1086 | 1094 | zManifestUuid); |
| 1087 | 1095 | if(isSbs){ |
| @@ -1090,91 +1098,33 @@ | ||
| 1090 | 1098 | CX("<pre class='udiff'>%b</pre>",&out); |
| 1091 | 1099 | } |
| 1092 | 1100 | CX("</div><!--.fileedit-diff-->\n"); |
| 1093 | 1101 | blob_reset(&orig); |
| 1094 | 1102 | 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; | |
| 1176 | 1126 | } |
| 1177 | 1127 | |
| 1178 | 1128 | /* |
| 1179 | 1129 | ** WEBPAGE: fileedit |
| 1180 | 1130 | ** |
| @@ -1212,10 +1162,11 @@ | ||
| 1212 | 1162 | int previewLn = P("preview_ln")!=0; /* Line number mode */ |
| 1213 | 1163 | int previewHtmlHeight = 0; /* iframe height (EMs) */ |
| 1214 | 1164 | int previewRenderMode = FE_RENDER_GUESS; /* preview mode */ |
| 1215 | 1165 | char * zFileUuid = 0; /* File content UUID */ |
| 1216 | 1166 | Blob err = empty_blob; /* Error report */ |
| 1167 | + Blob submitResult = empty_blob; /* Error report */ | |
| 1217 | 1168 | const char * zFlagCheck = 0; /* Temp url flag holder */ |
| 1218 | 1169 | Blob endScript = empty_blob; /* Script code to run at the |
| 1219 | 1170 | end. This content will be |
| 1220 | 1171 | combined into a single JS |
| 1221 | 1172 | function call, thus each |
| @@ -1285,27 +1236,19 @@ | ||
| 1285 | 1236 | cimi.zFilename = mprintf("%s",zFilename); |
| 1286 | 1237 | zFileMime = mimetype_from_name(zFilename); |
| 1287 | 1238 | |
| 1288 | 1239 | /* Find the repo-side file entry or fail... */ |
| 1289 | 1240 | 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); | |
| 1302 | 1242 | if(!zFileUuid){ |
| 1303 | 1243 | fail((&err,"Checkin [%S] does not contain file: " |
| 1304 | 1244 | "<code>%h</code>", |
| 1305 | 1245 | cimi.zParentUuid, zFilename)); |
| 1306 | 1246 | } |
| 1247 | + else if(PERM_LNK==cimi.filePerm){ | |
| 1248 | + fail((&err,"Editing symlinks is not permitted.")); | |
| 1249 | + } | |
| 1307 | 1250 | frid = fast_uuid_to_rid(zFileUuid); |
| 1308 | 1251 | assert(frid); |
| 1309 | 1252 | |
| 1310 | 1253 | /* Read file content from submit request or repo... */ |
| 1311 | 1254 | if(zContent==0){ |
| @@ -1318,11 +1261,100 @@ | ||
| 1318 | 1261 | if(looks_like_binary(&cimi.fileContent)){ |
| 1319 | 1262 | fail((&err,"File appears to be binary. Cannot edit: " |
| 1320 | 1263 | "<code>%h</code>",zFilename)); |
| 1321 | 1264 | } |
| 1322 | 1265 | |
| 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 | + } | |
| 1324 | 1356 | |
| 1325 | 1357 | CX("<h1>Editing:</h1>"); |
| 1326 | 1358 | CX("<p class='fileedit-hint'>"); |
| 1327 | 1359 | CX("File: " |
| 1328 | 1360 | "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] " |
| @@ -1338,12 +1370,12 @@ | ||
| 1338 | 1370 | "(Clicking the permalink will reload the page and discard " |
| 1339 | 1371 | "all edits!)", |
| 1340 | 1372 | zFilename, cimi.zParentUuid, |
| 1341 | 1373 | zFilename, cimi.zParentUuid); |
| 1342 | 1374 | 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 " | |
| 1345 | 1377 | "repo.</p>\n"); |
| 1346 | 1378 | |
| 1347 | 1379 | CX("<form action='%R/fileedit#options' method='POST' " |
| 1348 | 1380 | "class='fileedit'>\n"); |
| 1349 | 1381 | |
| @@ -1368,78 +1400,49 @@ | ||
| 1368 | 1400 | CX("<fieldset class='fileedit-options' id='options'>" |
| 1369 | 1401 | "<legend>Options</legend><div>" |
| 1370 | 1402 | /* Chrome does not sanely lay out multiple |
| 1371 | 1403 | ** fieldset children after the <legend>, so |
| 1372 | 1404 | ** 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 | 1405 | style_labeled_checkbox("dry_run", "Dry-run?", "1", |
| 1381 | 1406 | "In dry-run mode, the Save button performs " |
| 1382 | 1407 | "all work needed for saving but then rolls " |
| 1383 | 1408 | "back the transaction, and thus does not " |
| 1384 | 1409 | "really save.", |
| 1385 | 1410 | cimi.flags & CIMINI_DRY_RUN); |
| 1386 | - if(P("allow_fork")!=0){ | |
| 1387 | - cimi.flags |= CIMINI_ALLOW_FORK; | |
| 1388 | - } | |
| 1389 | 1411 | style_labeled_checkbox("allow_fork", "Allow fork?", "1", |
| 1390 | 1412 | "Allow saving to create a fork?", |
| 1391 | 1413 | cimi.flags & CIMINI_ALLOW_FORK); |
| 1392 | - if(P("allow_older")!=0){ | |
| 1393 | - cimi.flags |= CIMINI_ALLOW_OLDER; | |
| 1394 | - } | |
| 1395 | 1414 | style_labeled_checkbox("allow_older", "Allow older?", "1", |
| 1396 | 1415 | "Allow saving against a parent version " |
| 1397 | 1416 | "which has a newer timestamp?", |
| 1398 | 1417 | cimi.flags & CIMINI_ALLOW_OLDER); |
| 1399 | - if(P("exec_bit")!=0){ | |
| 1400 | - cimi.filePerm = PERM_EXE; | |
| 1401 | - } | |
| 1402 | 1418 | style_labeled_checkbox("exec_bit", "Executable?", "1", |
| 1403 | 1419 | "Set the executable bit?", |
| 1404 | 1420 | PERM_EXE==cimi.filePerm); |
| 1405 | - if(P("allow_merge_conflict")!=0){ | |
| 1406 | - cimi.flags |= CIMINI_ALLOW_MERGE_MARKER; | |
| 1407 | - } | |
| 1408 | 1421 | style_labeled_checkbox("allow_merge_conflict", |
| 1409 | 1422 | "Allow merge conflict markers?", "1", |
| 1410 | 1423 | "Allow saving even if the content contains " |
| 1411 | 1424 | "what appear to be fossil merge conflict " |
| 1412 | 1425 | "markers?", |
| 1413 | 1426 | cimi.flags & CIMINI_ALLOW_MERGE_MARKER); |
| 1414 | - if(P("prefer_delta")!=0){ | |
| 1415 | - cimi.flags |= CIMINI_PREFER_DELTA; | |
| 1416 | - } | |
| 1417 | 1427 | style_labeled_checkbox("prefer_delta", |
| 1418 | 1428 | "Prefer delta manifest?", "1", |
| 1419 | 1429 | "Will create a delta manifest, instead of " |
| 1420 | 1430 | "baseline, if conditions are favorable to do " |
| 1421 | 1431 | "so. This option is only a suggestion.", |
| 1422 | 1432 | 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); | |
| 1441 | 1444 | |
| 1442 | 1445 | CX("</div></fieldset>") /* end of checkboxes */; |
| 1443 | 1446 | |
| 1444 | 1447 | /******* Comment *******/ |
| 1445 | 1448 | CX("<a id='comment'></a>"); |
| @@ -1446,25 +1449,23 @@ | ||
| 1446 | 1449 | CX("<fieldset><legend>Commit message</legend><div>"); |
| 1447 | 1450 | CX("<textarea name='comment' rows='3' cols='80'>"); |
| 1448 | 1451 | /* ^^^ adding the 'required' attribute means we cannot even submit |
| 1449 | 1452 | ** for PREVIEW mode if it's empty :/. */ |
| 1450 | 1453 | if(zComment && *zComment){ |
| 1451 | - CX("%h"/*%h? %s?*/, zComment); | |
| 1454 | + CX("%h", zComment); | |
| 1452 | 1455 | } |
| 1453 | 1456 | CX("</textarea>\n"); |
| 1454 | 1457 | CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup " |
| 1455 | 1458 | "syntax.</div>\n"/*TODO: select for fossil/md/plain text*/); |
| 1456 | 1459 | CX("</div></fieldset>\n"); |
| 1457 | 1460 | |
| 1458 | - | |
| 1459 | - | |
| 1460 | 1461 | /******* Buttons *******/ |
| 1461 | 1462 | CX("<a id='buttons'></a>"); |
| 1462 | 1463 | CX("<fieldset class='fileedit-options'>" |
| 1463 | 1464 | "<legend>Tell the server to...</legend><div>"); |
| 1464 | 1465 | CX("<button type='submit' name='submit' value='%d'>" |
| 1465 | - "Save</button>", SUBMIT_SAVE); | |
| 1466 | + "Commit</button>", SUBMIT_SAVE); | |
| 1466 | 1467 | CX("<button type='submit' name='submit' value='%d'>" |
| 1467 | 1468 | "Preview</button>", SUBMIT_PREVIEW); |
| 1468 | 1469 | { |
| 1469 | 1470 | /* Preview rendering mode selection... */ |
| 1470 | 1471 | previewRenderMode = atoi(PD("preview_render_mode","0")); |
| @@ -1512,12 +1513,29 @@ | ||
| 1512 | 1513 | "Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED); |
| 1513 | 1514 | CX("</div></fieldset>"); |
| 1514 | 1515 | |
| 1515 | 1516 | /******* End of form *******/ |
| 1516 | 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 | + } | |
| 1517 | 1534 | |
| 1518 | 1535 | /* Dynamically populate the editor... */ |
| 1536 | + fileedit_emit_script_fetch(); | |
| 1519 | 1537 | if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){ |
| 1520 | 1538 | char const * zQuoted = 0; |
| 1521 | 1539 | if(blob_size(&cimi.fileContent)>0){ |
| 1522 | 1540 | db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent); |
| 1523 | 1541 | db_step(&stmt); |
| @@ -1530,117 +1548,42 @@ | ||
| 1530 | 1548 | if(stmt.pStmt){ |
| 1531 | 1549 | db_finalize(&stmt); |
| 1532 | 1550 | } |
| 1533 | 1551 | }else if(2==loadMode){ |
| 1534 | 1552 | assert(submitMode==SUBMIT_NONE); |
| 1535 | - fileedit_emit_script_fetch(); | |
| 1536 | 1553 | blob_appendf(&endScript, |
| 1537 | - "window.fossilFetch('raw/%s',{" | |
| 1554 | + "window.fossil.fetch('raw/%s',{" | |
| 1538 | 1555 | "onload: (r)=>document.getElementById('fileedit-content')" |
| 1539 | 1556 | ".value=r," |
| 1540 | 1557 | "onerror:()=>document.getElementById('fileedit-content')" |
| 1541 | 1558 | ".value=" |
| 1542 | 1559 | "'Error loading content'" |
| 1543 | 1560 | "});\n", zFileUuid); |
| 1544 | 1561 | } |
| 1545 | 1562 | |
| 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 | 1563 | end_footer: |
| 1624 | 1564 | zContent = 0; |
| 1625 | 1565 | fossil_free(zFileUuid); |
| 1626 | 1566 | if(stmt.pStmt){ |
| 1627 | 1567 | db_finalize(&stmt); |
| 1628 | 1568 | } |
| 1629 | 1569 | 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); | |
| 1632 | 1574 | } |
| 1575 | + blob_reset(&submitResult); | |
| 1633 | 1576 | blob_reset(&err); |
| 1634 | 1577 | CheckinMiniInfo_cleanup(&cimi); |
| 1635 | 1578 | if(blob_size(&endScript)>0){ |
| 1636 | - fileedit_emit_script(0); | |
| 1579 | + style_emit_script_tag(0); | |
| 1637 | 1580 | CX("(function(){\n"); |
| 1638 | 1581 | CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n", |
| 1639 | 1582 | &endScript); |
| 1640 | 1583 | CX("})();"); |
| 1641 | - fileedit_emit_script(1); | |
| 1584 | + style_emit_script_tag(1); | |
| 1642 | 1585 | } |
| 1643 | 1586 | db_end_transaction(0/*noting that dry-run mode will have already |
| 1644 | 1587 | ** set this to rollback mode. */); |
| 1645 | 1588 | style_footer(); |
| 1646 | 1589 | } |
| 1647 | 1590 |
| --- 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> → 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> → 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 @@ | ||
| 1295 | 1295 | } |
| 1296 | 1296 | |
| 1297 | 1297 | #if INTERFACE |
| 1298 | 1298 | # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);} |
| 1299 | 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 | +} | |
| 1300 | 1434 |
| --- 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 |