Fossil SCM

Added fileedit-glob setting (/fileedit whitelist) and honor it in /fileedit. Added mini-checkin options to specifically convert EOLs to Unix or Windows styles. /fileedit saving now more or less works, but still needs to update the 'r' value after editing or else we're stuck at the old parent version.

stephan 2020-05-02 15:03 UTC checkin-without-checkout
Commit 33861414aefee477466cbe224687e650e11ba4868a6fbc4b6639fc1bc4028cfc
+273 -80
--- src/checkin.c
+++ src/checkin.c
@@ -2772,16 +2772,24 @@
27722772
** Indicates that the content of the newly-checked-in file is
27732773
** converted, if needed, to use the same EOL style as the previous
27742774
** version of that file. Only the in-memory/in-repo copies are
27752775
** affected, not the original file (if any).
27762776
*/
2777
-CIMINI_CONVERT_EOL = 1<<5,
2777
+CIMINI_CONVERT_EOL_INHERIT = 1<<5,
2778
+/*
2779
+** Indicates that the input's EOLs should be converted to Unix-style.
2780
+*/
2781
+CIMINI_CONVERT_EOL_UNIX = 1<<6,
2782
+/*
2783
+** Indicates that the input's EOLs should be converted to Windows-style.
2784
+*/
2785
+CIMINI_CONVERT_EOL_WINDOWS = 1<<7,
27782786
/*
27792787
** A hint to checkin_mini() to "prefer" creation of a delta manifest.
27802788
** It may decide not to for various reasons.
27812789
*/
2782
-CIMINI_PREFER_DELTA = 1<<6,
2790
+CIMINI_PREFER_DELTA = 1<<8,
27832791
/*
27842792
** A "stronger hint" to checkin_mini() to prefer creation of a delta
27852793
** manifest if it at all can. It will decide not to only if creation
27862794
** of a delta is not a realistic option. For this to work, it must be
27872795
** set together with the CIMINI_PREFER_DELTA flag, but the two cannot
@@ -2792,18 +2800,18 @@
27922800
** grounds of efficiency (e.g. not generating a delta if the parent
27932801
** non-delta only has a few F-cards).
27942802
**
27952803
** The forbid-delta-manifests repo config option trumps this.
27962804
*/
2797
-CIMINI_STRONGLY_PREFER_DELTA = 1<<7,
2805
+CIMINI_STRONGLY_PREFER_DELTA = 1<<9,
27982806
/*
27992807
** Tells checkin_mini() to permit the addition of a new file. Normally
28002808
** this is disabled because there are many cases where it could cause
28012809
** the inadvertent addition of a new file when an update to an
28022810
** existing was intended, as a side-effect of name-case differences.
28032811
*/
2804
-CIMINI_ALLOW_NEW_FILE = 1<<8
2812
+CIMINI_ALLOW_NEW_FILE = 1<<10
28052813
};
28062814
28072815
/*
28082816
** Initializes p to a known-valid default state.
28092817
*/
@@ -2841,10 +2849,21 @@
28412849
case PERM_EXE: return " x";
28422850
case PERM_LNK: return " l";
28432851
default: return "";
28442852
}
28452853
}
2854
+
2855
+/*
2856
+** Given a ManifestFile permission string (or NULL), it returns one of
2857
+** PERM_REG, PERM_EXE, or PERM_LNK.
2858
+*/
2859
+static int mfile_permstr_int(const char *zPerm){
2860
+ if(!zPerm || !*zPerm) return PERM_REG;
2861
+ else if(strstr(zPerm,"x")) return PERM_EXE;
2862
+ else if(strstr(zPerm,"l")) return PERM_LNK;
2863
+ else return PERM_REG/*???*/;
2864
+}
28462865
28472866
static const char * mfile_perm_mstring(const ManifestFile * p){
28482867
return mfile_permint_mstring(manifest_file_mperm(p));
28492868
}
28502869
@@ -3063,19 +3082,21 @@
30633082
** - pCI->zDate is normalized to/replaced with a valid date/time
30643083
** string. If its original value cannot be validated then
30653084
** this function fails. If pCI->zDate is NULL, the current time
30663085
** is used.
30673086
**
3068
-** - If the CIMINI_CONVERT_EOL flag is set, pCI->fileContent appears
3069
-** to be plain text, and its line-ending style differs from its
3070
-** previous version, it is converted to the same EOL style as the
3071
-** previous version. If this is done, the pCI->fileHash is
3072
-** re-computed. Note that only pCI->fileContent, not the original
3073
-** file, is affected by the conversion.
3087
+** - If the CIMINI_CONVERT_EOL_INHERIT flag is set,
3088
+** pCI->fileContent appears to be plain text, and its line-ending
3089
+** style differs from its previous version, it is converted to the
3090
+** same EOL style as the previous version. If this is done, the
3091
+** pCI->fileHash is re-computed. Note that only pCI->fileContent,
3092
+** not the original file, is affected by the conversion.
30743093
**
30753094
** - If pCI->fileHash is empty, this routine populates it with the
30763095
** repository's preferred hash algorithm.
3096
+**
3097
+** - pCI->comment may be converted to Unix-style newlines.
30773098
**
30783099
** pCI's ownership is not modified.
30793100
**
30803101
** This function validates several of the inputs and fails if any
30813102
** validation fails.
@@ -3174,10 +3195,19 @@
31743195
fossil_free(zDVal);
31753196
ci_err((pErr,"Invalid timestamp string: %s", pCI->zDate));
31763197
}
31773198
fossil_free(pCI->zDate);
31783199
pCI->zDate = zDVal;
3200
+ }
3201
+ { /* Confirm that only one EOL policy is in place. */
3202
+ int n = 0;
3203
+ if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags) ++n;
3204
+ if(CIMINI_CONVERT_EOL_UNIX & pCI->flags) ++n;
3205
+ if(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags) ++n;
3206
+ if(n>1){
3207
+ ci_err((pErr,"More than 1 EOL conversion policy was specified."));
3208
+ }
31793209
}
31803210
/* Potential TODOs include:
31813211
**
31823212
** - Commit allows an empty checkin only with a flag, but we
31833213
** currently disallow it entirely. Conform with commit?
@@ -3212,13 +3242,15 @@
32123242
}
32133243
if(zFilePrev){
32143244
prevFRid = fast_uuid_to_rid(zFilePrev->zUuid);
32153245
}
32163246
3217
- if((CIMINI_CONVERT_EOL & pCI->flags)
3218
- && zFilePrev!=0
3219
- && blob_size(&pCI->fileContent)>0){
3247
+ if(((CIMINI_CONVERT_EOL_INHERIT & pCI->flags)
3248
+ || (CIMINI_CONVERT_EOL_UNIX & pCI->flags)
3249
+ || (CIMINI_CONVERT_EOL_WINDOWS & pCI->flags))
3250
+ && blob_size(&pCI->fileContent)>0
3251
+ ){
32203252
/* Confirm that the new content has the same EOL style as its
32213253
** predecessor and convert it, if needed, to the same style. Note
32223254
** that this inherently runs a risk of breaking content,
32233255
** e.g. string literals which contain embedded newlines. Note that
32243256
** HTML5 specifies that form-submitted TEXTAREA content gets
@@ -3234,51 +3266,72 @@
32343266
*/
32353267
const int pseudoBinary = LOOK_LONG | LOOK_NUL;
32363268
const int lookFlags = LOOK_CRLF | pseudoBinary;
32373269
const int lookNew = looks_like_utf8( &pCI->fileContent, lookFlags );
32383270
if(!(pseudoBinary & lookNew)){
3239
- Blob contentPrev = empty_blob;
3240
- int lookOrig, nOrig;
3241
- content_get(prevFRid, &contentPrev);
3242
- lookOrig = looks_like_utf8(&contentPrev, lookFlags);
3243
- nOrig = blob_size(&contentPrev);
3244
- blob_reset(&contentPrev);
3245
- if(nOrig>0 && lookOrig!=lookNew){
3246
- /* If there is a newline-style mismatch, adjust the new
3247
- ** content version to the previous style, then re-hash the
3248
- ** content. Note that this means that what we insert is NOT
3249
- ** what's in the filesystem.
3250
- */
3251
- int rehash = 0;
3252
- if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){
3253
- /* Old has Unix-style, new has Windows-style. */
3254
- blob_to_lf_only(&pCI->fileContent);
3255
- rehash = 1;
3256
- }else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){
3257
- /* Old has Windows-style, new has Unix-style. */
3258
- blob_add_cr(&pCI->fileContent);
3259
- rehash = 1;
3260
- }
3261
- if(rehash!=0){
3262
- hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3263
- }
3264
- }
3265
- }
3266
- }
3271
+ int rehash = 0;
3272
+ if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags){
3273
+ Blob contentPrev = empty_blob;
3274
+ int lookOrig, nOrig;
3275
+ content_get(prevFRid, &contentPrev);
3276
+ lookOrig = looks_like_utf8(&contentPrev, lookFlags);
3277
+ nOrig = blob_size(&contentPrev);
3278
+ blob_reset(&contentPrev);
3279
+ if(nOrig>0 && lookOrig!=lookNew){
3280
+ /* If there is a newline-style mismatch, adjust the new
3281
+ ** content version to the previous style, then re-hash the
3282
+ ** content. Note that this means that what we insert is NOT
3283
+ ** what's in the filesystem.
3284
+ */
3285
+ if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){
3286
+ /* Old has Unix-style, new has Windows-style. */
3287
+ blob_to_lf_only(&pCI->fileContent);
3288
+ rehash = 1;
3289
+ }else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){
3290
+ /* Old has Windows-style, new has Unix-style. */
3291
+ blob_add_cr(&pCI->fileContent);
3292
+ rehash = 1;
3293
+ }
3294
+ }
3295
+ }else{
3296
+ const int oldSize = blob_size(&pCI->fileContent);
3297
+ if(CIMINI_CONVERT_EOL_UNIX & pCI->flags){
3298
+ blob_to_lf_only(&pCI->fileContent);
3299
+ }else{
3300
+ blob_add_cr(&pCI->fileContent);
3301
+ assert(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags);
3302
+ }
3303
+ if(blob_size(&pCI->fileContent)!=oldSize){
3304
+ rehash = 1;
3305
+ }
3306
+ }
3307
+ if(rehash!=0){
3308
+ hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3309
+ }
3310
+ }
3311
+ }/* end EOL conversion */
3312
+
32673313
if(blob_size(&pCI->fileHash)==0){
32683314
/* Hash the content if it's not done already... */
32693315
hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
32703316
assert(blob_size(&pCI->fileHash)>0);
32713317
}
32723318
if(zFilePrev){
3273
- /* Has this file been changed since its previous commit? */
3319
+ /* Has this file been changed since its previous commit? Note
3320
+ ** that we have to delay this check until after the potentially
3321
+ ** expensive EOL conversion. */
32743322
assert(blob_size(&pCI->fileHash));
32753323
if(0==fossil_strcmp(zFilePrev->zUuid, blob_str(&pCI->fileHash))
32763324
&& manifest_file_mperm(zFilePrev)==pCI->filePerm){
32773325
ci_err((pErr,"File is unchanged. Not saving."));
32783326
}
32793327
}
3328
+#if 1
3329
+ /* Do we really want to normalize comment EOLs? Web-posting will
3330
+ ** submit them in CRLF format. */
3331
+ blob_to_lf_only(&pCI->comment);
3332
+#endif
32803333
/* Create, save, deltify, and crosslink the manifest... */
32813334
if(create_manifest_mini(&mf, pCI, pErr)==0){
32823335
return 0;
32833336
}
32843337
isPrivate = content_is_private(pCI->pParent->rid);
@@ -3411,12 +3464,12 @@
34113464
cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
34123465
}
34133466
if(find_option("allow-older",0,0)!=0){
34143467
cimi.flags |= CIMINI_ALLOW_OLDER;
34153468
}
3416
- if(find_option("convert-eol",0,0)!=0){
3417
- cimi.flags |= CIMINI_CONVERT_EOL;
3469
+ if(find_option("convert-eol-prev",0,0)!=0){
3470
+ cimi.flags |= CIMINI_CONVERT_EOL_INHERIT;
34183471
}
34193472
if(find_option("delta",0,0)!=0){
34203473
cimi.flags |= CIMINI_PREFER_DELTA;
34213474
}
34223475
if(find_option("delta2",0,0)!=0){
@@ -3502,22 +3555,81 @@
35023555
CheckinMiniInfo_cleanup(&cimi);
35033556
}
35043557
35053558
35063559
/*
3507
-** Returns true if the given filename qualified for online editing
3508
-** by the current user.
3560
+** Returns true if the given filename qualifies for online editing by
3561
+** the current user, else returns false.
35093562
**
3510
-** Currently only looks at the user's permissions, pending decisions
3511
-** on whether we want to filter them based on a glob list or mimetype
3512
-** list.
3563
+** Editing requires that the user have the Write permission and that
3564
+** the filename match the glob defined by the fileedit-glob setting.
3565
+** A missing or empty value for that glob disables all editing.
35133566
*/
3514
-int file_is_online_editable(const char *zFilename){
3515
- if(g.perm.Write){
3516
- return 1;
3567
+int fileedit_is_editable(const char *zFilename){
3568
+ static Glob * pGlobs = 0;
3569
+ static int once = 0;
3570
+ if(0==g.perm.Write || zFilename==0 || *zFilename==0
3571
+ || (once!=0 && pGlobs==0)){
3572
+ return 0;
3573
+ }else if(0==pGlobs){
3574
+ char * zGlobs = db_get("fileedit-glob",0);
3575
+ once = 1;
3576
+ if(0==zGlobs) return 0;
3577
+ pGlobs = glob_create(zGlobs);
3578
+ fossil_free(zGlobs);
3579
+ once = 1;
3580
+ }
3581
+ return glob_match(pGlobs, zFilename);
3582
+}
3583
+
3584
+static void fileedit_emit_script(int phase){
3585
+ if(0==phase){
3586
+ fossil_print("<script nonce='%s'>", style_nonce());
3587
+ }else{
3588
+ fossil_print("</script>\n");
3589
+ }
3590
+}
3591
+static void fileedit_emit_script_fetch(){
3592
+#define fp fossil_print
3593
+ fileedit_emit_script(0);
3594
+ fp("window.fossilFetch = function(path,opt){\n");
3595
+ fp(" if('function'===typeof opt){\n");
3596
+ fp(" opt={onload:opt};\n");
3597
+ fp(" }else{\n");
3598
+ fp(" opt=opt||{onload:function(r){console.debug('response:',r)}}\n");
3599
+ fp(" }\n");
3600
+ fp(" const url='%R/'+path, x=new XMLHttpRequest();\n");
3601
+ fp(" x.open(opt.method||'GET', url, true);\n");
3602
+ fp(" x.responseType=opt.responseType||'text';\n");
3603
+ fp(" if(opt.onload){\n");
3604
+ fp(" x.onload = function(e){\n");
3605
+ fp(" if(200!==this.status){\n");
3606
+ fp(" if(opt.onerror) opt.onerror(e);\n");
3607
+ fp(" return;\n");
3608
+ fp(" }\n");
3609
+ fp(" opt.onload(this.response);\n");
3610
+ fp(" }\n");
3611
+ fp(" }\n");
3612
+ fp(" x.send();");
3613
+ fp("};\n");
3614
+ fileedit_emit_script(1);
3615
+#undef fp
3616
+};
3617
+
3618
+static void fileedit_checkbox(const char *zFieldName,
3619
+ const char * zLabel,
3620
+ const char * zValue,
3621
+ const char * zTip,
3622
+ int isChecked){
3623
+ fossil_print("<span class='input-with-label'");
3624
+ if(zTip && *zTip){
3625
+ fossil_print(" title='%h'", zTip);
35173626
}
3518
- return 0;
3627
+ fossil_print("><input type='checkbox' name='%s' value='%T'%s/>",
3628
+ zFieldName,
3629
+ zValue ? zValue : "", isChecked ? " checked" : "");
3630
+ fossil_print("<span>%h</span></span>", zLabel);
35193631
}
35203632
35213633
/*
35223634
** WEBPAGE: fileedit
35233635
**
@@ -3545,12 +3657,13 @@
35453657
const char * zContent = P("content"); /* file content */
35463658
const char * zComment = P("comment"); /* checkin comment */
35473659
CheckinMiniInfo cimi; /* Checkin state */
35483660
int submitMode = 0; /* See mapping below */
35493661
char * zRevResolved = 0; /* Resolved zRev */
3550
- int vid; /* checkin rid */
3662
+ int vid, newVid = 0; /* checkin rid */
35513663
char * zFileUuid = 0; /* File content UUID */
3664
+ int frid = 0; /* File content rid */
35523665
Blob err = empty_blob; /* Error report */
35533666
const char * zFlagCheck = 0; /* Temp url flag holder */
35543667
Stmt stmt = empty_Stmt;
35553668
#define fail(EXPR) blob_appendf EXPR; goto end_footer
35563669
@@ -3558,10 +3671,13 @@
35583671
if( !g.perm.Write ){
35593672
login_needed(g.anon.Write);
35603673
return;
35613674
}
35623675
CheckinMiniInfo_init(&cimi);
3676
+ submitMode = atoi(PD("submit","0"))
3677
+ /* Submit modes: 0=initial request,
3678
+ ** 1=submit (save), 2=preview, 3=diff */;
35633679
zFlagCheck = P("comment");
35643680
if(zFlagCheck){
35653681
cimi.zMimetype = mprintf("%s",zFlagCheck);
35663682
zFlagCheck = 0;
35673683
}
@@ -3572,12 +3688,17 @@
35723688
** - Preview button + view
35733689
**
35743690
** - Diff button + view
35753691
**
35763692
*/
3693
+ style_header("File Editor");
35773694
if(!zRev || !*zRev || !zFilename || !*zFilename){
35783695
fail((&err,"Missing required URL parameters."));
3696
+ }
3697
+ if(0==fileedit_is_editable(zFilename)){
3698
+ fail((&err,"Filename is disallowed by the fileedit-glob "
3699
+ "repository setting."));
35793700
}
35803701
vid = symbolic_name_to_rid(zRev, "ci");
35813702
if(0==vid){
35823703
fail((&err,"Could not resolve checkin version."));
35833704
}
@@ -3587,26 +3708,26 @@
35873708
db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
35883709
"WHERE filename=%Q %s AND checkinID=%d",
35893710
zFilename, filename_collation(), vid);
35903711
if(SQLITE_ROW==db_step(&stmt)){
35913712
const char * zPerm = db_column_text(&stmt, 1);
3592
- const int isLink = zPerm ? strstr(zPerm,"l")!=0 : 0;
3593
- if(isLink){
3713
+ cimi.filePerm = mfile_permstr_int(zPerm);
3714
+ if(PERM_LNK==cimi.filePerm){
35943715
fail((&err,"Editing symlinks is not permitted."));
35953716
}
35963717
zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
35973718
}
35983719
db_finalize(&stmt);
35993720
if(!zFileUuid){
36003721
fail((&err,"Checkin [%S] does not contain file: %h",
36013722
zRevResolved, zFilename));
36023723
}
3724
+ frid = fast_uuid_to_rid(zFileUuid);
3725
+ assert(frid);
36033726
36043727
/* Read file content from submit request or repo... */
36053728
if(zContent==0){
3606
- const int frid = fast_uuid_to_rid(zFileUuid);
3607
- assert(frid);
36083729
content_get(frid, &cimi.fileContent);
36093730
zContent = blob_size(&cimi.fileContent)
36103731
? blob_str(&cimi.fileContent) : NULL;
36113732
}else{
36123733
blob_init(&cimi.fileContent,zContent,-1);
@@ -3615,14 +3736,15 @@
36153736
fail((&err,"File appears to be binary. Cannot edit: %h",
36163737
zFilename));
36173738
}
36183739
36193740
/* All set. Here we go... */
3620
- style_header("File Editor");
36213741
#define fp fossil_print
36223742
/* ^^^ Appologies, Richard, but the @ form plays havoc with emacs */
36233743
3744
+ fileedit_emit_script_fetch();
3745
+
36243746
fp("<h1>Editing: %h</h1>",zFilename);
36253747
fp("<p class='hint'>Permalink: "
36263748
"<a href='%R/fileedit?file=%T&r=%s'>"
36273749
"%R/fileedit?file=%T&r=%s</a></p>",
36283750
zFilename, zRev, zFilename, zRev);
@@ -3646,94 +3768,165 @@
36463768
fp("<div class='hint'>Comments use the Fossil wiki markup "
36473769
"syntax.</div>"/*TODO: radiobuttons for fossil/me/plain text*/);
36483770
36493771
/******* Content *******/
36503772
fp("<h3>Content</h3>\n");
3651
- fp("<textarea name='content' rows='20' cols='80'>");
3652
- if(zContent && *zContent){
3653
- fp("%h", zContent);
3654
- }
3773
+ fp("<textarea name='content' id='fileedit-content' "
3774
+ "rows='20' cols='80'>");
3775
+ fp("Loading...");
36553776
fp("</textarea>\n");
3656
-
36573777
/******* Flags/options *******/
36583778
fp("<fieldset class='fileedit-options'>"
36593779
"<legend>Many checkboxes are TODO</legend><div>"
36603780
/* Chrome does not sanely lay out multiple
36613781
** fieldset children after the <legend>, so
36623782
** a containing div is necessary. */);
36633783
/*
36643784
** TODO: Put checkboxes here...
36653785
**
3666
- ** allow-fork, dry-run, convert-eol, allow-merge-conflict,
3667
- ** set-exec-bit, date-override, allow-older (in case server time is
3786
+ ** dry-run, convert-eol
3787
+ ** date-override, allow-older (in case server time is
36683788
** messed up or someone checked something in w/ a future timestamp),
36693789
** prefer-delta, strongly-prefer-delta (undocumented - for
36703790
** development/admin use only).
36713791
*/
3672
- fp("</div></fieldset>");
3792
+ if(0==submitMode || P("dry_run")!=0){
3793
+ cimi.flags |= CIMINI_DRY_RUN;
3794
+ }
3795
+ fileedit_checkbox("dry_run", "Dry-run?", "1",
3796
+ "In dry-run mode, do not really save.",
3797
+ cimi.flags & CIMINI_DRY_RUN);
3798
+ if(P("allow_fork")!=0){
3799
+ cimi.flags |= CIMINI_ALLOW_FORK;
3800
+ }
3801
+ fileedit_checkbox("allow_fork", "Allow fork?", "1",
3802
+ "Allow saving to create a fork?",
3803
+ cimi.flags & CIMINI_ALLOW_FORK);
3804
+ if(P("exec_bit")!=0){
3805
+ cimi.filePerm = PERM_EXE;
3806
+ }
3807
+ fileedit_checkbox("exec_bit", "Executable?", "1",
3808
+ "Set executable bit?",
3809
+ PERM_EXE==cimi.filePerm);
3810
+ if(P("allow_merge_conflict")!=0){
3811
+ cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
3812
+ }
3813
+ fileedit_checkbox("allow_merge_conflict",
3814
+ "Allow merge conflict markers?", "1",
3815
+ "Allow saving even if the content contains what "
3816
+ "appear to be fossil merge conflict markers?",
3817
+ cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
3818
+ {/* EOL conversion policy... */
3819
+ const int eolMode = submitMode==0 ? 0 : atoi(PD("eol","0"));
3820
+ switch(eolMode){
3821
+ case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
3822
+ case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
3823
+ default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
3824
+ }
3825
+ fp("<select name='eol' "
3826
+ "title='EOL conversion policy, noting that form-processing "
3827
+ "may implicitly change the line endings of the input.'>");
3828
+ fp("<option value='0'%s>Inherit EOLs</option>",
3829
+ eolMode==0 ? " selected" : "");
3830
+ fp("<option value='1'%s/>Unix EOLs</option>",
3831
+ eolMode==1 ? " selected" : "");
3832
+ fp("<option value='2'%s>Windows EOLs</option>",
3833
+ eolMode==2 ? " selected" : "");
3834
+ fp("</select>");
3835
+ }
3836
+ fp("</div></fieldset>") /* end of checkboxes */;
36733837
36743838
/******* Buttons *******/
36753839
fp("<fieldset class='fileedit-options'>"
36763840
"<legend>Several buttons are TODO</legend><div>");
36773841
fp("<button type='submit' name='submit' value='1'>"
3678
- "Submit (dry-run)</button>");
3842
+ "Submit</button>");
36793843
fp("<button type='submit' name='submit' value='2'>"
36803844
"Preview (TODO)</button>");
36813845
fp("<button type='submit' name='submit' value='3'>"
36823846
"Diff (TODO)</button>");
36833847
fp("</div></fieldset>");
36843848
36853849
/******* End of form *******/
36863850
fp("</form>\n");
3851
+
3852
+ /* Populate doc...
3853
+ ** To avoid all escaping-related issues, we have to do this one
3854
+ ** of two ways:
3855
+ **
3856
+ ** 1) Fetch the content via AJAX. That only works if the content
3857
+ ** is already in the db, but not for edited versions.
3858
+ **
3859
+ ** 2) Store the content as JSON and feed it into the textarea
3860
+ ** using JavaScript.
3861
+ */
3862
+ fileedit_emit_script(0);
3863
+ {
3864
+ char const * zQuoted = 0;
3865
+ if(blob_size(&cimi.fileContent)>0){
3866
+ db_prepare(&stmt, "SELECT json_quote(%B)",&cimi.fileContent);
3867
+ db_step(&stmt);
3868
+ zQuoted = db_column_text(&stmt,0);
3869
+ }
3870
+ fp("document.getElementById('fileedit-content').value=%s;",
3871
+ zQuoted ? zQuoted : "''");
3872
+ if(stmt.pStmt){
3873
+ db_finalize(&stmt);
3874
+ }
3875
+ }
3876
+ fileedit_emit_script(1);
3877
+
36873878
zContent = 0;
36883879
fossil_free(zRevResolved);
36893880
fossil_free(zFileUuid);
36903881
3691
- submitMode = atoi(PD("submit","0"))
3692
- /*
3693
- ** Submit modes: 1=submit (save), 2=preview, 3=diff
3694
- */;
36953882
if(1==submitMode/*save*/){
36963883
Blob manifest = empty_blob;
36973884
int rc;
36983885
36993886
/* TODO: pull these flags from P() */
3700
- cimi.flags |= CIMINI_DRY_RUN;
3701
- cimi.flags |= CIMINI_CONVERT_EOL;
37023887
cimi.flags |= CIMINI_PREFER_DELTA;
37033888
/*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
37043889
37053890
db_begin_transaction();
37063891
cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
37073892
assert(cimi.pParent && "We know vid is valid.");
37083893
cimi.zFilename = mprintf("%s",zFilename);
37093894
cimi.pMfOut = &manifest;
3710
- cimi.filePerm = PERM_REG;
37113895
if(zComment && *zComment){
37123896
blob_append(&cimi.comment, zComment, -1);
37133897
}
3714
- rc = checkin_mini(&cimi, 0, &err);
3715
- if(rc!=0){
3716
- fp("<h3>Manifest</h3><pre><code>%h</code></pre>",
3717
- blob_str(&manifest));
3898
+ rc = checkin_mini(&cimi, &newVid, &err);
3899
+ if(newVid!=0){
3900
+ char * zNewUuid = rid_to_uuid(newVid);
3901
+ fp("<h3>Manifest%s: %S</h3><pre>"
3902
+ "<code class='fileedit-manifest'>%h</code>"
3903
+ "</pre>",
3904
+ (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
3905
+ zNewUuid, blob_str(&manifest));
3906
+ fossil_free(zNewUuid);
37183907
}
37193908
blob_reset(&manifest);
37203909
db_end_transaction(rc ? 0 : 1);
37213910
}else if(2==submitMode/*preview*/){
37223911
/* TODO */
3912
+ fail((&err,"Preview mode is still TODO."));
37233913
}else if(3==submitMode/*diff*/){
3724
- /* TODO */
3914
+ fail((&err,"Diff mode is still TODO."));
37253915
}else{
37263916
/* Ignore invalid submitMode value */
37273917
goto end_footer;
37283918
}
37293919
37303920
end_footer:
3921
+ if(stmt.pStmt){
3922
+ db_finalize(&stmt);
3923
+ }
37313924
if(blob_size(&err)){
37323925
fp("<div class='fileedit-error-report'>%h</div>",
37333926
blob_str(&err));
37343927
}
37353928
blob_reset(&err);
37363929
CheckinMiniInfo_cleanup(&cimi);
37373930
style_footer();
37383931
#undef fp
37393932
}
37403933
--- src/checkin.c
+++ src/checkin.c
@@ -2772,16 +2772,24 @@
2772 ** Indicates that the content of the newly-checked-in file is
2773 ** converted, if needed, to use the same EOL style as the previous
2774 ** version of that file. Only the in-memory/in-repo copies are
2775 ** affected, not the original file (if any).
2776 */
2777 CIMINI_CONVERT_EOL = 1<<5,
 
 
 
 
 
 
 
 
2778 /*
2779 ** A hint to checkin_mini() to "prefer" creation of a delta manifest.
2780 ** It may decide not to for various reasons.
2781 */
2782 CIMINI_PREFER_DELTA = 1<<6,
2783 /*
2784 ** A "stronger hint" to checkin_mini() to prefer creation of a delta
2785 ** manifest if it at all can. It will decide not to only if creation
2786 ** of a delta is not a realistic option. For this to work, it must be
2787 ** set together with the CIMINI_PREFER_DELTA flag, but the two cannot
@@ -2792,18 +2800,18 @@
2792 ** grounds of efficiency (e.g. not generating a delta if the parent
2793 ** non-delta only has a few F-cards).
2794 **
2795 ** The forbid-delta-manifests repo config option trumps this.
2796 */
2797 CIMINI_STRONGLY_PREFER_DELTA = 1<<7,
2798 /*
2799 ** Tells checkin_mini() to permit the addition of a new file. Normally
2800 ** this is disabled because there are many cases where it could cause
2801 ** the inadvertent addition of a new file when an update to an
2802 ** existing was intended, as a side-effect of name-case differences.
2803 */
2804 CIMINI_ALLOW_NEW_FILE = 1<<8
2805 };
2806
2807 /*
2808 ** Initializes p to a known-valid default state.
2809 */
@@ -2841,10 +2849,21 @@
2841 case PERM_EXE: return " x";
2842 case PERM_LNK: return " l";
2843 default: return "";
2844 }
2845 }
 
 
 
 
 
 
 
 
 
 
 
2846
2847 static const char * mfile_perm_mstring(const ManifestFile * p){
2848 return mfile_permint_mstring(manifest_file_mperm(p));
2849 }
2850
@@ -3063,19 +3082,21 @@
3063 ** - pCI->zDate is normalized to/replaced with a valid date/time
3064 ** string. If its original value cannot be validated then
3065 ** this function fails. If pCI->zDate is NULL, the current time
3066 ** is used.
3067 **
3068 ** - If the CIMINI_CONVERT_EOL flag is set, pCI->fileContent appears
3069 ** to be plain text, and its line-ending style differs from its
3070 ** previous version, it is converted to the same EOL style as the
3071 ** previous version. If this is done, the pCI->fileHash is
3072 ** re-computed. Note that only pCI->fileContent, not the original
3073 ** file, is affected by the conversion.
3074 **
3075 ** - If pCI->fileHash is empty, this routine populates it with the
3076 ** repository's preferred hash algorithm.
 
 
3077 **
3078 ** pCI's ownership is not modified.
3079 **
3080 ** This function validates several of the inputs and fails if any
3081 ** validation fails.
@@ -3174,10 +3195,19 @@
3174 fossil_free(zDVal);
3175 ci_err((pErr,"Invalid timestamp string: %s", pCI->zDate));
3176 }
3177 fossil_free(pCI->zDate);
3178 pCI->zDate = zDVal;
 
 
 
 
 
 
 
 
 
3179 }
3180 /* Potential TODOs include:
3181 **
3182 ** - Commit allows an empty checkin only with a flag, but we
3183 ** currently disallow it entirely. Conform with commit?
@@ -3212,13 +3242,15 @@
3212 }
3213 if(zFilePrev){
3214 prevFRid = fast_uuid_to_rid(zFilePrev->zUuid);
3215 }
3216
3217 if((CIMINI_CONVERT_EOL & pCI->flags)
3218 && zFilePrev!=0
3219 && blob_size(&pCI->fileContent)>0){
 
 
3220 /* Confirm that the new content has the same EOL style as its
3221 ** predecessor and convert it, if needed, to the same style. Note
3222 ** that this inherently runs a risk of breaking content,
3223 ** e.g. string literals which contain embedded newlines. Note that
3224 ** HTML5 specifies that form-submitted TEXTAREA content gets
@@ -3234,51 +3266,72 @@
3234 */
3235 const int pseudoBinary = LOOK_LONG | LOOK_NUL;
3236 const int lookFlags = LOOK_CRLF | pseudoBinary;
3237 const int lookNew = looks_like_utf8( &pCI->fileContent, lookFlags );
3238 if(!(pseudoBinary & lookNew)){
3239 Blob contentPrev = empty_blob;
3240 int lookOrig, nOrig;
3241 content_get(prevFRid, &contentPrev);
3242 lookOrig = looks_like_utf8(&contentPrev, lookFlags);
3243 nOrig = blob_size(&contentPrev);
3244 blob_reset(&contentPrev);
3245 if(nOrig>0 && lookOrig!=lookNew){
3246 /* If there is a newline-style mismatch, adjust the new
3247 ** content version to the previous style, then re-hash the
3248 ** content. Note that this means that what we insert is NOT
3249 ** what's in the filesystem.
3250 */
3251 int rehash = 0;
3252 if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){
3253 /* Old has Unix-style, new has Windows-style. */
3254 blob_to_lf_only(&pCI->fileContent);
3255 rehash = 1;
3256 }else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){
3257 /* Old has Windows-style, new has Unix-style. */
3258 blob_add_cr(&pCI->fileContent);
3259 rehash = 1;
3260 }
3261 if(rehash!=0){
3262 hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3263 }
3264 }
3265 }
3266 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3267 if(blob_size(&pCI->fileHash)==0){
3268 /* Hash the content if it's not done already... */
3269 hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3270 assert(blob_size(&pCI->fileHash)>0);
3271 }
3272 if(zFilePrev){
3273 /* Has this file been changed since its previous commit? */
 
 
3274 assert(blob_size(&pCI->fileHash));
3275 if(0==fossil_strcmp(zFilePrev->zUuid, blob_str(&pCI->fileHash))
3276 && manifest_file_mperm(zFilePrev)==pCI->filePerm){
3277 ci_err((pErr,"File is unchanged. Not saving."));
3278 }
3279 }
 
 
 
 
 
3280 /* Create, save, deltify, and crosslink the manifest... */
3281 if(create_manifest_mini(&mf, pCI, pErr)==0){
3282 return 0;
3283 }
3284 isPrivate = content_is_private(pCI->pParent->rid);
@@ -3411,12 +3464,12 @@
3411 cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
3412 }
3413 if(find_option("allow-older",0,0)!=0){
3414 cimi.flags |= CIMINI_ALLOW_OLDER;
3415 }
3416 if(find_option("convert-eol",0,0)!=0){
3417 cimi.flags |= CIMINI_CONVERT_EOL;
3418 }
3419 if(find_option("delta",0,0)!=0){
3420 cimi.flags |= CIMINI_PREFER_DELTA;
3421 }
3422 if(find_option("delta2",0,0)!=0){
@@ -3502,22 +3555,81 @@
3502 CheckinMiniInfo_cleanup(&cimi);
3503 }
3504
3505
3506 /*
3507 ** Returns true if the given filename qualified for online editing
3508 ** by the current user.
3509 **
3510 ** Currently only looks at the user's permissions, pending decisions
3511 ** on whether we want to filter them based on a glob list or mimetype
3512 ** list.
3513 */
3514 int file_is_online_editable(const char *zFilename){
3515 if(g.perm.Write){
3516 return 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3517 }
3518 return 0;
 
 
 
3519 }
3520
3521 /*
3522 ** WEBPAGE: fileedit
3523 **
@@ -3545,12 +3657,13 @@
3545 const char * zContent = P("content"); /* file content */
3546 const char * zComment = P("comment"); /* checkin comment */
3547 CheckinMiniInfo cimi; /* Checkin state */
3548 int submitMode = 0; /* See mapping below */
3549 char * zRevResolved = 0; /* Resolved zRev */
3550 int vid; /* checkin rid */
3551 char * zFileUuid = 0; /* File content UUID */
 
3552 Blob err = empty_blob; /* Error report */
3553 const char * zFlagCheck = 0; /* Temp url flag holder */
3554 Stmt stmt = empty_Stmt;
3555 #define fail(EXPR) blob_appendf EXPR; goto end_footer
3556
@@ -3558,10 +3671,13 @@
3558 if( !g.perm.Write ){
3559 login_needed(g.anon.Write);
3560 return;
3561 }
3562 CheckinMiniInfo_init(&cimi);
 
 
 
3563 zFlagCheck = P("comment");
3564 if(zFlagCheck){
3565 cimi.zMimetype = mprintf("%s",zFlagCheck);
3566 zFlagCheck = 0;
3567 }
@@ -3572,12 +3688,17 @@
3572 ** - Preview button + view
3573 **
3574 ** - Diff button + view
3575 **
3576 */
 
3577 if(!zRev || !*zRev || !zFilename || !*zFilename){
3578 fail((&err,"Missing required URL parameters."));
 
 
 
 
3579 }
3580 vid = symbolic_name_to_rid(zRev, "ci");
3581 if(0==vid){
3582 fail((&err,"Could not resolve checkin version."));
3583 }
@@ -3587,26 +3708,26 @@
3587 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
3588 "WHERE filename=%Q %s AND checkinID=%d",
3589 zFilename, filename_collation(), vid);
3590 if(SQLITE_ROW==db_step(&stmt)){
3591 const char * zPerm = db_column_text(&stmt, 1);
3592 const int isLink = zPerm ? strstr(zPerm,"l")!=0 : 0;
3593 if(isLink){
3594 fail((&err,"Editing symlinks is not permitted."));
3595 }
3596 zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
3597 }
3598 db_finalize(&stmt);
3599 if(!zFileUuid){
3600 fail((&err,"Checkin [%S] does not contain file: %h",
3601 zRevResolved, zFilename));
3602 }
 
 
3603
3604 /* Read file content from submit request or repo... */
3605 if(zContent==0){
3606 const int frid = fast_uuid_to_rid(zFileUuid);
3607 assert(frid);
3608 content_get(frid, &cimi.fileContent);
3609 zContent = blob_size(&cimi.fileContent)
3610 ? blob_str(&cimi.fileContent) : NULL;
3611 }else{
3612 blob_init(&cimi.fileContent,zContent,-1);
@@ -3615,14 +3736,15 @@
3615 fail((&err,"File appears to be binary. Cannot edit: %h",
3616 zFilename));
3617 }
3618
3619 /* All set. Here we go... */
3620 style_header("File Editor");
3621 #define fp fossil_print
3622 /* ^^^ Appologies, Richard, but the @ form plays havoc with emacs */
3623
 
 
3624 fp("<h1>Editing: %h</h1>",zFilename);
3625 fp("<p class='hint'>Permalink: "
3626 "<a href='%R/fileedit?file=%T&r=%s'>"
3627 "%R/fileedit?file=%T&r=%s</a></p>",
3628 zFilename, zRev, zFilename, zRev);
@@ -3646,94 +3768,165 @@
3646 fp("<div class='hint'>Comments use the Fossil wiki markup "
3647 "syntax.</div>"/*TODO: radiobuttons for fossil/me/plain text*/);
3648
3649 /******* Content *******/
3650 fp("<h3>Content</h3>\n");
3651 fp("<textarea name='content' rows='20' cols='80'>");
3652 if(zContent && *zContent){
3653 fp("%h", zContent);
3654 }
3655 fp("</textarea>\n");
3656
3657 /******* Flags/options *******/
3658 fp("<fieldset class='fileedit-options'>"
3659 "<legend>Many checkboxes are TODO</legend><div>"
3660 /* Chrome does not sanely lay out multiple
3661 ** fieldset children after the <legend>, so
3662 ** a containing div is necessary. */);
3663 /*
3664 ** TODO: Put checkboxes here...
3665 **
3666 ** allow-fork, dry-run, convert-eol, allow-merge-conflict,
3667 ** set-exec-bit, date-override, allow-older (in case server time is
3668 ** messed up or someone checked something in w/ a future timestamp),
3669 ** prefer-delta, strongly-prefer-delta (undocumented - for
3670 ** development/admin use only).
3671 */
3672 fp("</div></fieldset>");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3673
3674 /******* Buttons *******/
3675 fp("<fieldset class='fileedit-options'>"
3676 "<legend>Several buttons are TODO</legend><div>");
3677 fp("<button type='submit' name='submit' value='1'>"
3678 "Submit (dry-run)</button>");
3679 fp("<button type='submit' name='submit' value='2'>"
3680 "Preview (TODO)</button>");
3681 fp("<button type='submit' name='submit' value='3'>"
3682 "Diff (TODO)</button>");
3683 fp("</div></fieldset>");
3684
3685 /******* End of form *******/
3686 fp("</form>\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3687 zContent = 0;
3688 fossil_free(zRevResolved);
3689 fossil_free(zFileUuid);
3690
3691 submitMode = atoi(PD("submit","0"))
3692 /*
3693 ** Submit modes: 1=submit (save), 2=preview, 3=diff
3694 */;
3695 if(1==submitMode/*save*/){
3696 Blob manifest = empty_blob;
3697 int rc;
3698
3699 /* TODO: pull these flags from P() */
3700 cimi.flags |= CIMINI_DRY_RUN;
3701 cimi.flags |= CIMINI_CONVERT_EOL;
3702 cimi.flags |= CIMINI_PREFER_DELTA;
3703 /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
3704
3705 db_begin_transaction();
3706 cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
3707 assert(cimi.pParent && "We know vid is valid.");
3708 cimi.zFilename = mprintf("%s",zFilename);
3709 cimi.pMfOut = &manifest;
3710 cimi.filePerm = PERM_REG;
3711 if(zComment && *zComment){
3712 blob_append(&cimi.comment, zComment, -1);
3713 }
3714 rc = checkin_mini(&cimi, 0, &err);
3715 if(rc!=0){
3716 fp("<h3>Manifest</h3><pre><code>%h</code></pre>",
3717 blob_str(&manifest));
 
 
 
 
 
3718 }
3719 blob_reset(&manifest);
3720 db_end_transaction(rc ? 0 : 1);
3721 }else if(2==submitMode/*preview*/){
3722 /* TODO */
 
3723 }else if(3==submitMode/*diff*/){
3724 /* TODO */
3725 }else{
3726 /* Ignore invalid submitMode value */
3727 goto end_footer;
3728 }
3729
3730 end_footer:
 
 
 
3731 if(blob_size(&err)){
3732 fp("<div class='fileedit-error-report'>%h</div>",
3733 blob_str(&err));
3734 }
3735 blob_reset(&err);
3736 CheckinMiniInfo_cleanup(&cimi);
3737 style_footer();
3738 #undef fp
3739 }
3740
--- src/checkin.c
+++ src/checkin.c
@@ -2772,16 +2772,24 @@
2772 ** Indicates that the content of the newly-checked-in file is
2773 ** converted, if needed, to use the same EOL style as the previous
2774 ** version of that file. Only the in-memory/in-repo copies are
2775 ** affected, not the original file (if any).
2776 */
2777 CIMINI_CONVERT_EOL_INHERIT = 1<<5,
2778 /*
2779 ** Indicates that the input's EOLs should be converted to Unix-style.
2780 */
2781 CIMINI_CONVERT_EOL_UNIX = 1<<6,
2782 /*
2783 ** Indicates that the input's EOLs should be converted to Windows-style.
2784 */
2785 CIMINI_CONVERT_EOL_WINDOWS = 1<<7,
2786 /*
2787 ** A hint to checkin_mini() to "prefer" creation of a delta manifest.
2788 ** It may decide not to for various reasons.
2789 */
2790 CIMINI_PREFER_DELTA = 1<<8,
2791 /*
2792 ** A "stronger hint" to checkin_mini() to prefer creation of a delta
2793 ** manifest if it at all can. It will decide not to only if creation
2794 ** of a delta is not a realistic option. For this to work, it must be
2795 ** set together with the CIMINI_PREFER_DELTA flag, but the two cannot
@@ -2792,18 +2800,18 @@
2800 ** grounds of efficiency (e.g. not generating a delta if the parent
2801 ** non-delta only has a few F-cards).
2802 **
2803 ** The forbid-delta-manifests repo config option trumps this.
2804 */
2805 CIMINI_STRONGLY_PREFER_DELTA = 1<<9,
2806 /*
2807 ** Tells checkin_mini() to permit the addition of a new file. Normally
2808 ** this is disabled because there are many cases where it could cause
2809 ** the inadvertent addition of a new file when an update to an
2810 ** existing was intended, as a side-effect of name-case differences.
2811 */
2812 CIMINI_ALLOW_NEW_FILE = 1<<10
2813 };
2814
2815 /*
2816 ** Initializes p to a known-valid default state.
2817 */
@@ -2841,10 +2849,21 @@
2849 case PERM_EXE: return " x";
2850 case PERM_LNK: return " l";
2851 default: return "";
2852 }
2853 }
2854
2855 /*
2856 ** Given a ManifestFile permission string (or NULL), it returns one of
2857 ** PERM_REG, PERM_EXE, or PERM_LNK.
2858 */
2859 static int mfile_permstr_int(const char *zPerm){
2860 if(!zPerm || !*zPerm) return PERM_REG;
2861 else if(strstr(zPerm,"x")) return PERM_EXE;
2862 else if(strstr(zPerm,"l")) return PERM_LNK;
2863 else return PERM_REG/*???*/;
2864 }
2865
2866 static const char * mfile_perm_mstring(const ManifestFile * p){
2867 return mfile_permint_mstring(manifest_file_mperm(p));
2868 }
2869
@@ -3063,19 +3082,21 @@
3082 ** - pCI->zDate is normalized to/replaced with a valid date/time
3083 ** string. If its original value cannot be validated then
3084 ** this function fails. If pCI->zDate is NULL, the current time
3085 ** is used.
3086 **
3087 ** - If the CIMINI_CONVERT_EOL_INHERIT flag is set,
3088 ** pCI->fileContent appears to be plain text, and its line-ending
3089 ** style differs from its previous version, it is converted to the
3090 ** same EOL style as the previous version. If this is done, the
3091 ** pCI->fileHash is re-computed. Note that only pCI->fileContent,
3092 ** not the original file, is affected by the conversion.
3093 **
3094 ** - If pCI->fileHash is empty, this routine populates it with the
3095 ** repository's preferred hash algorithm.
3096 **
3097 ** - pCI->comment may be converted to Unix-style newlines.
3098 **
3099 ** pCI's ownership is not modified.
3100 **
3101 ** This function validates several of the inputs and fails if any
3102 ** validation fails.
@@ -3174,10 +3195,19 @@
3195 fossil_free(zDVal);
3196 ci_err((pErr,"Invalid timestamp string: %s", pCI->zDate));
3197 }
3198 fossil_free(pCI->zDate);
3199 pCI->zDate = zDVal;
3200 }
3201 { /* Confirm that only one EOL policy is in place. */
3202 int n = 0;
3203 if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags) ++n;
3204 if(CIMINI_CONVERT_EOL_UNIX & pCI->flags) ++n;
3205 if(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags) ++n;
3206 if(n>1){
3207 ci_err((pErr,"More than 1 EOL conversion policy was specified."));
3208 }
3209 }
3210 /* Potential TODOs include:
3211 **
3212 ** - Commit allows an empty checkin only with a flag, but we
3213 ** currently disallow it entirely. Conform with commit?
@@ -3212,13 +3242,15 @@
3242 }
3243 if(zFilePrev){
3244 prevFRid = fast_uuid_to_rid(zFilePrev->zUuid);
3245 }
3246
3247 if(((CIMINI_CONVERT_EOL_INHERIT & pCI->flags)
3248 || (CIMINI_CONVERT_EOL_UNIX & pCI->flags)
3249 || (CIMINI_CONVERT_EOL_WINDOWS & pCI->flags))
3250 && blob_size(&pCI->fileContent)>0
3251 ){
3252 /* Confirm that the new content has the same EOL style as its
3253 ** predecessor and convert it, if needed, to the same style. Note
3254 ** that this inherently runs a risk of breaking content,
3255 ** e.g. string literals which contain embedded newlines. Note that
3256 ** HTML5 specifies that form-submitted TEXTAREA content gets
@@ -3234,51 +3266,72 @@
3266 */
3267 const int pseudoBinary = LOOK_LONG | LOOK_NUL;
3268 const int lookFlags = LOOK_CRLF | pseudoBinary;
3269 const int lookNew = looks_like_utf8( &pCI->fileContent, lookFlags );
3270 if(!(pseudoBinary & lookNew)){
3271 int rehash = 0;
3272 if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags){
3273 Blob contentPrev = empty_blob;
3274 int lookOrig, nOrig;
3275 content_get(prevFRid, &contentPrev);
3276 lookOrig = looks_like_utf8(&contentPrev, lookFlags);
3277 nOrig = blob_size(&contentPrev);
3278 blob_reset(&contentPrev);
3279 if(nOrig>0 && lookOrig!=lookNew){
3280 /* If there is a newline-style mismatch, adjust the new
3281 ** content version to the previous style, then re-hash the
3282 ** content. Note that this means that what we insert is NOT
3283 ** what's in the filesystem.
3284 */
3285 if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){
3286 /* Old has Unix-style, new has Windows-style. */
3287 blob_to_lf_only(&pCI->fileContent);
3288 rehash = 1;
3289 }else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){
3290 /* Old has Windows-style, new has Unix-style. */
3291 blob_add_cr(&pCI->fileContent);
3292 rehash = 1;
3293 }
3294 }
3295 }else{
3296 const int oldSize = blob_size(&pCI->fileContent);
3297 if(CIMINI_CONVERT_EOL_UNIX & pCI->flags){
3298 blob_to_lf_only(&pCI->fileContent);
3299 }else{
3300 blob_add_cr(&pCI->fileContent);
3301 assert(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags);
3302 }
3303 if(blob_size(&pCI->fileContent)!=oldSize){
3304 rehash = 1;
3305 }
3306 }
3307 if(rehash!=0){
3308 hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3309 }
3310 }
3311 }/* end EOL conversion */
3312
3313 if(blob_size(&pCI->fileHash)==0){
3314 /* Hash the content if it's not done already... */
3315 hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
3316 assert(blob_size(&pCI->fileHash)>0);
3317 }
3318 if(zFilePrev){
3319 /* Has this file been changed since its previous commit? Note
3320 ** that we have to delay this check until after the potentially
3321 ** expensive EOL conversion. */
3322 assert(blob_size(&pCI->fileHash));
3323 if(0==fossil_strcmp(zFilePrev->zUuid, blob_str(&pCI->fileHash))
3324 && manifest_file_mperm(zFilePrev)==pCI->filePerm){
3325 ci_err((pErr,"File is unchanged. Not saving."));
3326 }
3327 }
3328 #if 1
3329 /* Do we really want to normalize comment EOLs? Web-posting will
3330 ** submit them in CRLF format. */
3331 blob_to_lf_only(&pCI->comment);
3332 #endif
3333 /* Create, save, deltify, and crosslink the manifest... */
3334 if(create_manifest_mini(&mf, pCI, pErr)==0){
3335 return 0;
3336 }
3337 isPrivate = content_is_private(pCI->pParent->rid);
@@ -3411,12 +3464,12 @@
3464 cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
3465 }
3466 if(find_option("allow-older",0,0)!=0){
3467 cimi.flags |= CIMINI_ALLOW_OLDER;
3468 }
3469 if(find_option("convert-eol-prev",0,0)!=0){
3470 cimi.flags |= CIMINI_CONVERT_EOL_INHERIT;
3471 }
3472 if(find_option("delta",0,0)!=0){
3473 cimi.flags |= CIMINI_PREFER_DELTA;
3474 }
3475 if(find_option("delta2",0,0)!=0){
@@ -3502,22 +3555,81 @@
3555 CheckinMiniInfo_cleanup(&cimi);
3556 }
3557
3558
3559 /*
3560 ** Returns true if the given filename qualifies for online editing by
3561 ** the current user, else returns false.
3562 **
3563 ** Editing requires that the user have the Write permission and that
3564 ** the filename match the glob defined by the fileedit-glob setting.
3565 ** A missing or empty value for that glob disables all editing.
3566 */
3567 int fileedit_is_editable(const char *zFilename){
3568 static Glob * pGlobs = 0;
3569 static int once = 0;
3570 if(0==g.perm.Write || zFilename==0 || *zFilename==0
3571 || (once!=0 && pGlobs==0)){
3572 return 0;
3573 }else if(0==pGlobs){
3574 char * zGlobs = db_get("fileedit-glob",0);
3575 once = 1;
3576 if(0==zGlobs) return 0;
3577 pGlobs = glob_create(zGlobs);
3578 fossil_free(zGlobs);
3579 once = 1;
3580 }
3581 return glob_match(pGlobs, zFilename);
3582 }
3583
3584 static void fileedit_emit_script(int phase){
3585 if(0==phase){
3586 fossil_print("<script nonce='%s'>", style_nonce());
3587 }else{
3588 fossil_print("</script>\n");
3589 }
3590 }
3591 static void fileedit_emit_script_fetch(){
3592 #define fp fossil_print
3593 fileedit_emit_script(0);
3594 fp("window.fossilFetch = function(path,opt){\n");
3595 fp(" if('function'===typeof opt){\n");
3596 fp(" opt={onload:opt};\n");
3597 fp(" }else{\n");
3598 fp(" opt=opt||{onload:function(r){console.debug('response:',r)}}\n");
3599 fp(" }\n");
3600 fp(" const url='%R/'+path, x=new XMLHttpRequest();\n");
3601 fp(" x.open(opt.method||'GET', url, true);\n");
3602 fp(" x.responseType=opt.responseType||'text';\n");
3603 fp(" if(opt.onload){\n");
3604 fp(" x.onload = function(e){\n");
3605 fp(" if(200!==this.status){\n");
3606 fp(" if(opt.onerror) opt.onerror(e);\n");
3607 fp(" return;\n");
3608 fp(" }\n");
3609 fp(" opt.onload(this.response);\n");
3610 fp(" }\n");
3611 fp(" }\n");
3612 fp(" x.send();");
3613 fp("};\n");
3614 fileedit_emit_script(1);
3615 #undef fp
3616 };
3617
3618 static void fileedit_checkbox(const char *zFieldName,
3619 const char * zLabel,
3620 const char * zValue,
3621 const char * zTip,
3622 int isChecked){
3623 fossil_print("<span class='input-with-label'");
3624 if(zTip && *zTip){
3625 fossil_print(" title='%h'", zTip);
3626 }
3627 fossil_print("><input type='checkbox' name='%s' value='%T'%s/>",
3628 zFieldName,
3629 zValue ? zValue : "", isChecked ? " checked" : "");
3630 fossil_print("<span>%h</span></span>", zLabel);
3631 }
3632
3633 /*
3634 ** WEBPAGE: fileedit
3635 **
@@ -3545,12 +3657,13 @@
3657 const char * zContent = P("content"); /* file content */
3658 const char * zComment = P("comment"); /* checkin comment */
3659 CheckinMiniInfo cimi; /* Checkin state */
3660 int submitMode = 0; /* See mapping below */
3661 char * zRevResolved = 0; /* Resolved zRev */
3662 int vid, newVid = 0; /* checkin rid */
3663 char * zFileUuid = 0; /* File content UUID */
3664 int frid = 0; /* File content rid */
3665 Blob err = empty_blob; /* Error report */
3666 const char * zFlagCheck = 0; /* Temp url flag holder */
3667 Stmt stmt = empty_Stmt;
3668 #define fail(EXPR) blob_appendf EXPR; goto end_footer
3669
@@ -3558,10 +3671,13 @@
3671 if( !g.perm.Write ){
3672 login_needed(g.anon.Write);
3673 return;
3674 }
3675 CheckinMiniInfo_init(&cimi);
3676 submitMode = atoi(PD("submit","0"))
3677 /* Submit modes: 0=initial request,
3678 ** 1=submit (save), 2=preview, 3=diff */;
3679 zFlagCheck = P("comment");
3680 if(zFlagCheck){
3681 cimi.zMimetype = mprintf("%s",zFlagCheck);
3682 zFlagCheck = 0;
3683 }
@@ -3572,12 +3688,17 @@
3688 ** - Preview button + view
3689 **
3690 ** - Diff button + view
3691 **
3692 */
3693 style_header("File Editor");
3694 if(!zRev || !*zRev || !zFilename || !*zFilename){
3695 fail((&err,"Missing required URL parameters."));
3696 }
3697 if(0==fileedit_is_editable(zFilename)){
3698 fail((&err,"Filename is disallowed by the fileedit-glob "
3699 "repository setting."));
3700 }
3701 vid = symbolic_name_to_rid(zRev, "ci");
3702 if(0==vid){
3703 fail((&err,"Could not resolve checkin version."));
3704 }
@@ -3587,26 +3708,26 @@
3708 db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
3709 "WHERE filename=%Q %s AND checkinID=%d",
3710 zFilename, filename_collation(), vid);
3711 if(SQLITE_ROW==db_step(&stmt)){
3712 const char * zPerm = db_column_text(&stmt, 1);
3713 cimi.filePerm = mfile_permstr_int(zPerm);
3714 if(PERM_LNK==cimi.filePerm){
3715 fail((&err,"Editing symlinks is not permitted."));
3716 }
3717 zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
3718 }
3719 db_finalize(&stmt);
3720 if(!zFileUuid){
3721 fail((&err,"Checkin [%S] does not contain file: %h",
3722 zRevResolved, zFilename));
3723 }
3724 frid = fast_uuid_to_rid(zFileUuid);
3725 assert(frid);
3726
3727 /* Read file content from submit request or repo... */
3728 if(zContent==0){
 
 
3729 content_get(frid, &cimi.fileContent);
3730 zContent = blob_size(&cimi.fileContent)
3731 ? blob_str(&cimi.fileContent) : NULL;
3732 }else{
3733 blob_init(&cimi.fileContent,zContent,-1);
@@ -3615,14 +3736,15 @@
3736 fail((&err,"File appears to be binary. Cannot edit: %h",
3737 zFilename));
3738 }
3739
3740 /* All set. Here we go... */
 
3741 #define fp fossil_print
3742 /* ^^^ Appologies, Richard, but the @ form plays havoc with emacs */
3743
3744 fileedit_emit_script_fetch();
3745
3746 fp("<h1>Editing: %h</h1>",zFilename);
3747 fp("<p class='hint'>Permalink: "
3748 "<a href='%R/fileedit?file=%T&r=%s'>"
3749 "%R/fileedit?file=%T&r=%s</a></p>",
3750 zFilename, zRev, zFilename, zRev);
@@ -3646,94 +3768,165 @@
3768 fp("<div class='hint'>Comments use the Fossil wiki markup "
3769 "syntax.</div>"/*TODO: radiobuttons for fossil/me/plain text*/);
3770
3771 /******* Content *******/
3772 fp("<h3>Content</h3>\n");
3773 fp("<textarea name='content' id='fileedit-content' "
3774 "rows='20' cols='80'>");
3775 fp("Loading...");
 
3776 fp("</textarea>\n");
 
3777 /******* Flags/options *******/
3778 fp("<fieldset class='fileedit-options'>"
3779 "<legend>Many checkboxes are TODO</legend><div>"
3780 /* Chrome does not sanely lay out multiple
3781 ** fieldset children after the <legend>, so
3782 ** a containing div is necessary. */);
3783 /*
3784 ** TODO: Put checkboxes here...
3785 **
3786 ** dry-run, convert-eol
3787 ** date-override, allow-older (in case server time is
3788 ** messed up or someone checked something in w/ a future timestamp),
3789 ** prefer-delta, strongly-prefer-delta (undocumented - for
3790 ** development/admin use only).
3791 */
3792 if(0==submitMode || P("dry_run")!=0){
3793 cimi.flags |= CIMINI_DRY_RUN;
3794 }
3795 fileedit_checkbox("dry_run", "Dry-run?", "1",
3796 "In dry-run mode, do not really save.",
3797 cimi.flags & CIMINI_DRY_RUN);
3798 if(P("allow_fork")!=0){
3799 cimi.flags |= CIMINI_ALLOW_FORK;
3800 }
3801 fileedit_checkbox("allow_fork", "Allow fork?", "1",
3802 "Allow saving to create a fork?",
3803 cimi.flags & CIMINI_ALLOW_FORK);
3804 if(P("exec_bit")!=0){
3805 cimi.filePerm = PERM_EXE;
3806 }
3807 fileedit_checkbox("exec_bit", "Executable?", "1",
3808 "Set executable bit?",
3809 PERM_EXE==cimi.filePerm);
3810 if(P("allow_merge_conflict")!=0){
3811 cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
3812 }
3813 fileedit_checkbox("allow_merge_conflict",
3814 "Allow merge conflict markers?", "1",
3815 "Allow saving even if the content contains what "
3816 "appear to be fossil merge conflict markers?",
3817 cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
3818 {/* EOL conversion policy... */
3819 const int eolMode = submitMode==0 ? 0 : atoi(PD("eol","0"));
3820 switch(eolMode){
3821 case 1: cimi.flags |= CIMINI_CONVERT_EOL_UNIX; break;
3822 case 2: cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; break;
3823 default: cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; break;
3824 }
3825 fp("<select name='eol' "
3826 "title='EOL conversion policy, noting that form-processing "
3827 "may implicitly change the line endings of the input.'>");
3828 fp("<option value='0'%s>Inherit EOLs</option>",
3829 eolMode==0 ? " selected" : "");
3830 fp("<option value='1'%s/>Unix EOLs</option>",
3831 eolMode==1 ? " selected" : "");
3832 fp("<option value='2'%s>Windows EOLs</option>",
3833 eolMode==2 ? " selected" : "");
3834 fp("</select>");
3835 }
3836 fp("</div></fieldset>") /* end of checkboxes */;
3837
3838 /******* Buttons *******/
3839 fp("<fieldset class='fileedit-options'>"
3840 "<legend>Several buttons are TODO</legend><div>");
3841 fp("<button type='submit' name='submit' value='1'>"
3842 "Submit</button>");
3843 fp("<button type='submit' name='submit' value='2'>"
3844 "Preview (TODO)</button>");
3845 fp("<button type='submit' name='submit' value='3'>"
3846 "Diff (TODO)</button>");
3847 fp("</div></fieldset>");
3848
3849 /******* End of form *******/
3850 fp("</form>\n");
3851
3852 /* Populate doc...
3853 ** To avoid all escaping-related issues, we have to do this one
3854 ** of two ways:
3855 **
3856 ** 1) Fetch the content via AJAX. That only works if the content
3857 ** is already in the db, but not for edited versions.
3858 **
3859 ** 2) Store the content as JSON and feed it into the textarea
3860 ** using JavaScript.
3861 */
3862 fileedit_emit_script(0);
3863 {
3864 char const * zQuoted = 0;
3865 if(blob_size(&cimi.fileContent)>0){
3866 db_prepare(&stmt, "SELECT json_quote(%B)",&cimi.fileContent);
3867 db_step(&stmt);
3868 zQuoted = db_column_text(&stmt,0);
3869 }
3870 fp("document.getElementById('fileedit-content').value=%s;",
3871 zQuoted ? zQuoted : "''");
3872 if(stmt.pStmt){
3873 db_finalize(&stmt);
3874 }
3875 }
3876 fileedit_emit_script(1);
3877
3878 zContent = 0;
3879 fossil_free(zRevResolved);
3880 fossil_free(zFileUuid);
3881
 
 
 
 
3882 if(1==submitMode/*save*/){
3883 Blob manifest = empty_blob;
3884 int rc;
3885
3886 /* TODO: pull these flags from P() */
 
 
3887 cimi.flags |= CIMINI_PREFER_DELTA;
3888 /*cimi.flags |= CIMINI_STRONGLY_PREFER_DELTA;*/
3889
3890 db_begin_transaction();
3891 cimi.pParent = manifest_get(vid, CFTYPE_MANIFEST, 0);
3892 assert(cimi.pParent && "We know vid is valid.");
3893 cimi.zFilename = mprintf("%s",zFilename);
3894 cimi.pMfOut = &manifest;
 
3895 if(zComment && *zComment){
3896 blob_append(&cimi.comment, zComment, -1);
3897 }
3898 rc = checkin_mini(&cimi, &newVid, &err);
3899 if(newVid!=0){
3900 char * zNewUuid = rid_to_uuid(newVid);
3901 fp("<h3>Manifest%s: %S</h3><pre>"
3902 "<code class='fileedit-manifest'>%h</code>"
3903 "</pre>",
3904 (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
3905 zNewUuid, blob_str(&manifest));
3906 fossil_free(zNewUuid);
3907 }
3908 blob_reset(&manifest);
3909 db_end_transaction(rc ? 0 : 1);
3910 }else if(2==submitMode/*preview*/){
3911 /* TODO */
3912 fail((&err,"Preview mode is still TODO."));
3913 }else if(3==submitMode/*diff*/){
3914 fail((&err,"Diff mode is still TODO."));
3915 }else{
3916 /* Ignore invalid submitMode value */
3917 goto end_footer;
3918 }
3919
3920 end_footer:
3921 if(stmt.pStmt){
3922 db_finalize(&stmt);
3923 }
3924 if(blob_size(&err)){
3925 fp("<div class='fileedit-error-report'>%h</div>",
3926 blob_str(&err));
3927 }
3928 blob_reset(&err);
3929 CheckinMiniInfo_cleanup(&cimi);
3930 style_footer();
3931 #undef fp
3932 }
3933
--- src/configure.c
+++ src/configure.c
@@ -148,10 +148,11 @@
148148
{ "parent-project-name", CONFIGSET_PROJ },
149149
{ "hash-policy", CONFIGSET_PROJ },
150150
{ "comment-format", CONFIGSET_PROJ },
151151
{ "mimetypes", CONFIGSET_PROJ },
152152
{ "forbid-delta-manifests", CONFIGSET_PROJ },
153
+ { "fileedit-glob", CONFIGSET_PROJ },
153154
154155
#ifdef FOSSIL_ENABLE_LEGACY_MV_RM
155156
{ "mv-rm-files", CONFIGSET_PROJ },
156157
#endif
157158
158159
--- src/configure.c
+++ src/configure.c
@@ -148,10 +148,11 @@
148 { "parent-project-name", CONFIGSET_PROJ },
149 { "hash-policy", CONFIGSET_PROJ },
150 { "comment-format", CONFIGSET_PROJ },
151 { "mimetypes", CONFIGSET_PROJ },
152 { "forbid-delta-manifests", CONFIGSET_PROJ },
 
153
154 #ifdef FOSSIL_ENABLE_LEGACY_MV_RM
155 { "mv-rm-files", CONFIGSET_PROJ },
156 #endif
157
158
--- src/configure.c
+++ src/configure.c
@@ -148,10 +148,11 @@
148 { "parent-project-name", CONFIGSET_PROJ },
149 { "hash-policy", CONFIGSET_PROJ },
150 { "comment-format", CONFIGSET_PROJ },
151 { "mimetypes", CONFIGSET_PROJ },
152 { "forbid-delta-manifests", CONFIGSET_PROJ },
153 { "fileedit-glob", CONFIGSET_PROJ },
154
155 #ifdef FOSSIL_ENABLE_LEGACY_MV_RM
156 { "mv-rm-files", CONFIGSET_PROJ },
157 #endif
158
159
+9
--- src/db.c
+++ src/db.c
@@ -3409,10 +3409,19 @@
34093409
** SETTING: exec-rel-paths boolean default=off
34103410
** When executing certain external commands (e.g. diff and
34113411
** gdiff), use relative paths.
34123412
*/
34133413
#endif
3414
+
3415
+/*
3416
+** SETTING: fileedit-glob width=40 block-text
3417
+** A comma- or newline-separated list of globs of filenames
3418
+** which are allowed to be edited using the /fileedit page.
3419
+** An empty list prohibits editing via that page. Note that
3420
+** it cannot edit binary files, so the glob should not
3421
+** contain any globs for, e.g., images or PDFs.
3422
+*/
34143423
/*
34153424
** SETTING: gdiff-command width=40 default=gdiff
34163425
** The value is an external command to run when performing a graphical
34173426
** diff. If undefined, text diff will be used.
34183427
*/
34193428
--- src/db.c
+++ src/db.c
@@ -3409,10 +3409,19 @@
3409 ** SETTING: exec-rel-paths boolean default=off
3410 ** When executing certain external commands (e.g. diff and
3411 ** gdiff), use relative paths.
3412 */
3413 #endif
 
 
 
 
 
 
 
 
 
3414 /*
3415 ** SETTING: gdiff-command width=40 default=gdiff
3416 ** The value is an external command to run when performing a graphical
3417 ** diff. If undefined, text diff will be used.
3418 */
3419
--- src/db.c
+++ src/db.c
@@ -3409,10 +3409,19 @@
3409 ** SETTING: exec-rel-paths boolean default=off
3410 ** When executing certain external commands (e.g. diff and
3411 ** gdiff), use relative paths.
3412 */
3413 #endif
3414
3415 /*
3416 ** SETTING: fileedit-glob width=40 block-text
3417 ** A comma- or newline-separated list of globs of filenames
3418 ** which are allowed to be edited using the /fileedit page.
3419 ** An empty list prohibits editing via that page. Note that
3420 ** it cannot edit binary files, so the glob should not
3421 ** contain any globs for, e.g., images or PDFs.
3422 */
3423 /*
3424 ** SETTING: gdiff-command width=40 default=gdiff
3425 ** The value is an external command to run when performing a graphical
3426 ** diff. If undefined, text diff will be used.
3427 */
3428
--- src/default_css.txt
+++ src/default_css.txt
@@ -881,13 +881,36 @@
881881
}
882882
.fileedit-form .hint {
883883
font-size: 80%;
884884
opacity: 0.75;
885885
}
886
-
887886
.fileedit-error-report {
888887
background: yellow;
889888
color: darkred;
890889
margin: 1em 0;
891890
padding: 0.5em;
892891
border-radius: 0.5em;
893892
}
893
+code.fileedit-manifest {
894
+ height: 12em;
895
+ scroll: auto;
896
+}
897
+.input-with-label {
898
+ border: 1px inset #808080;
899
+ border-radius: 0.5em;
900
+ padding: 0.25em 0.4em;
901
+ margin: 0 0.5em;
902
+ display: inline-block;
903
+}
904
+.input-with-label > input {
905
+ margin: 0;
906
+}
907
+.input-with-label > input[type=checkbox] {
908
+ vertical-align: sub;
909
+}
910
+.input-with-label > input[type=radio] {
911
+ vertical-align: sub;
912
+}
913
+.input-with-label > span {
914
+ margin: 0 0 0 0.4em;
915
+ vertical-align: text-bottom;
916
+}
894917
--- src/default_css.txt
+++ src/default_css.txt
@@ -881,13 +881,36 @@
881 }
882 .fileedit-form .hint {
883 font-size: 80%;
884 opacity: 0.75;
885 }
886
887 .fileedit-error-report {
888 background: yellow;
889 color: darkred;
890 margin: 1em 0;
891 padding: 0.5em;
892 border-radius: 0.5em;
893 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
--- src/default_css.txt
+++ src/default_css.txt
@@ -881,13 +881,36 @@
881 }
882 .fileedit-form .hint {
883 font-size: 80%;
884 opacity: 0.75;
885 }
 
886 .fileedit-error-report {
887 background: yellow;
888 color: darkred;
889 margin: 1em 0;
890 padding: 0.5em;
891 border-radius: 0.5em;
892 }
893 code.fileedit-manifest {
894 height: 12em;
895 scroll: auto;
896 }
897 .input-with-label {
898 border: 1px inset #808080;
899 border-radius: 0.5em;
900 padding: 0.25em 0.4em;
901 margin: 0 0.5em;
902 display: inline-block;
903 }
904 .input-with-label > input {
905 margin: 0;
906 }
907 .input-with-label > input[type=checkbox] {
908 vertical-align: sub;
909 }
910 .input-with-label > input[type=radio] {
911 vertical-align: sub;
912 }
913 .input-with-label > span {
914 margin: 0 0 0 0.4em;
915 vertical-align: text-bottom;
916 }
917

Keyboard Shortcuts

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