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