Fossil

fileedit.c at trunk
Login

File src/fileedit.c on branch trunk


     1  /*
     2  ** Copyright (c) 2020 D. Richard Hipp
     3  **
     4  ** This program is free software; you can redistribute it and/or
     5  ** modify it under the terms of the Simplified BSD License (also
     6  ** known as the "2-Clause License" or "FreeBSD License".)
     7  **
     8  ** This program is distributed in the hope that it will be useful,
     9  ** but without any warranty; without even the implied warranty of
    10  ** merchantability or fitness for a particular purpose.
    11  **
    12  ** Author contact information:
    13  **   drh@hwaci.com
    14  **   http://www.hwaci.com/drh/
    15  **
    16  *******************************************************************************
    17  **
    18  ** This file contains code for the /fileedit page and related bits.
    19  */
    20  #include "config.h"
    21  #include "fileedit.h"
    22  #include <assert.h>
    23  #include <stdarg.h>
    24  
    25  /*
    26  ** State for the "mini-checkin" infrastructure, which enables the
    27  ** ability to commit changes to a single file without a checkout
    28  ** db, e.g. for use via an HTTP request.
    29  **
    30  ** Use CheckinMiniInfo_init() to cleanly initialize one to a known
    31  ** valid/empty default state.
    32  **
    33  ** Memory for all non-const pointer members is owned by the
    34  ** CheckinMiniInfo instance, unless explicitly noted otherwise, and is
    35  ** freed by CheckinMiniInfo_cleanup(). Similarly, each instance owns
    36  ** any memory for its own Blob members, but NOT for its pointers to
    37  ** blobs.
    38  */
    39  struct CheckinMiniInfo {
    40    Manifest * pParent;  /* parent checkin. Memory is owned by this
    41                            object. */
    42    char *zParentUuid;   /* Full UUID of pParent */
    43    char *zFilename;     /* Name of single file to commit. Must be
    44                            relative to the top of the repo. */
    45    Blob fileContent;    /* Content of file referred to by zFilename. */
    46    Blob fileHash;       /* Hash of this->fileContent, using the repo's
    47                            preferred hash method. */
    48    Blob comment;        /* Check-in comment text */
    49    char *zCommentMimetype;  /* Mimetype of comment. May be NULL */
    50    char *zUser;         /* User name */
    51    char *zDate;         /* Optionally force this date string (anything
    52                            supported by date_in_standard_format()).
    53                            Maybe be NULL. */
    54    Blob *pMfOut;        /* If not NULL, checkin_mini() will write a
    55                            copy of the generated manifest here. This
    56                            memory is NOT owned by CheckinMiniInfo. */
    57    int filePerm;        /* Permissions (via file_perm()) of the input
    58                            file. We need to store this before calling
    59                            checkin_mini() because the real input file
    60                            name may differ from the repo-centric
    61                            this->zFilename, and checkin_mini() requires
    62                            the permissions of the original file. For
    63                            web commits, set this to PERM_REG or (when
    64                            editing executable scripts) PERM_EXE before
    65                            calling checkin_mini(). */
    66    int flags;           /* Bitmask of fossil_cimini_flags. */
    67  };
    68  typedef struct CheckinMiniInfo CheckinMiniInfo;
    69  
    70  /*
    71  ** CheckinMiniInfo::flags values.
    72  */
    73  enum fossil_cimini_flags {
    74  /*
    75  ** Must have a value of 0. All other flags have unspecified values.
    76  */
    77  CIMINI_NONE = 0,
    78  /*
    79  ** Tells checkin_mini() to use dry-run mode.
    80  */
    81  CIMINI_DRY_RUN = 1,
    82  /*
    83  ** Tells checkin_mini() to allow forking from a non-leaf commit.
    84  */
    85  CIMINI_ALLOW_FORK = 1<<1,
    86  /*
    87  ** Tells checkin_mini() to dump its generated manifest to stdout.
    88  */
    89  CIMINI_DUMP_MANIFEST = 1<<2,
    90  
    91  /*
    92  ** By default, content containing what appears to be a merge conflict
    93  ** marker is not permitted. This flag relaxes that requirement.
    94  */
    95  CIMINI_ALLOW_MERGE_MARKER = 1<<3,
    96  
    97  /*
    98  ** By default mini-checkins are not allowed to be "older"
    99  ** than their parent. i.e. they may not have a timestamp
   100  ** which predates their parent. This flag bypasses that
   101  ** check.
   102  */
   103  CIMINI_ALLOW_OLDER = 1<<4,
   104  
   105  /*
   106  ** Indicates that the content of the newly-checked-in file is
   107  ** converted, if needed, to use the same EOL style as the previous
   108  ** version of that file. Only the in-memory/in-repo copies are
   109  ** affected, not the original file (if any).
   110  */
   111  CIMINI_CONVERT_EOL_INHERIT = 1<<5,
   112  /*
   113  ** Indicates that the input's EOLs should be converted to Unix-style.
   114  */
   115  CIMINI_CONVERT_EOL_UNIX = 1<<6,
   116  /*
   117  ** Indicates that the input's EOLs should be converted to Windows-style.
   118  */
   119  CIMINI_CONVERT_EOL_WINDOWS = 1<<7,
   120  /*
   121  ** A hint to checkin_mini() to "prefer" creation of a delta manifest.
   122  ** It may decide not to for various reasons.
   123  */
   124  CIMINI_PREFER_DELTA = 1<<8,
   125  /*
   126  ** A "stronger hint" to checkin_mini() to prefer creation of a delta
   127  ** manifest if it at all can. It will decide not to only if creation
   128  ** of a delta is not a realistic option or if it's forbitted by the
   129  ** forbid-delta-manifests repo config option. For this to work, it
   130  ** must be set together with the CIMINI_PREFER_DELTA flag, but the two
   131  ** cannot be combined in this enum.
   132  **
   133  ** This option is ONLY INTENDED FOR TESTING, used in bypassing
   134  ** heuristics which may otherwise disable generation of a delta on the
   135  ** grounds of efficiency (e.g. not generating a delta if the parent
   136  ** non-delta only has a few F-cards).
   137  */
   138  CIMINI_STRONGLY_PREFER_DELTA = 1<<9,
   139  /*
   140  ** Tells checkin_mini() to permit the addition of a new file. Normally
   141  ** this is disabled because there are hypothetically many cases where
   142  ** it could cause the inadvertent addition of a new file when an
   143  ** update to an existing was intended, as a side-effect of name-case
   144  ** differences.
   145  */
   146  CIMINI_ALLOW_NEW_FILE = 1<<10
   147  };
   148  
   149  /*
   150  ** Initializes p to a known-valid default state.
   151  */
   152  static void CheckinMiniInfo_init( CheckinMiniInfo * p ){
   153    memset(p, 0, sizeof(CheckinMiniInfo));
   154    p->flags = CIMINI_NONE;
   155    p->filePerm = -1;
   156    p->comment = p->fileContent = p->fileHash = empty_blob;
   157  }
   158  
   159  /*
   160  ** Frees all memory owned by p, but does not free p.
   161   */
   162  static void CheckinMiniInfo_cleanup( CheckinMiniInfo * p ){
   163    blob_reset(&p->comment);
   164    blob_reset(&p->fileContent);
   165    blob_reset(&p->fileHash);
   166    if(p->pParent){
   167      manifest_destroy(p->pParent);
   168    }
   169    fossil_free(p->zFilename);
   170    fossil_free(p->zDate);
   171    fossil_free(p->zParentUuid);
   172    fossil_free(p->zCommentMimetype);
   173    fossil_free(p->zUser);
   174    CheckinMiniInfo_init(p);
   175  }
   176  
   177  /*
   178  ** Internal helper which returns an F-card perms string suitable for
   179  ** writing as-is into a manifest. If it's not empty, it includes a
   180  ** leading space to separate it from the F-card's hash field.
   181  */
   182  static const char * mfile_permint_mstring(int perm){
   183    switch(perm){
   184      case PERM_EXE: return " x";
   185      case PERM_LNK: return " l";
   186      default: return "";
   187    }
   188  }
   189  
   190  /*
   191  ** Given a ManifestFile permission string (or NULL), it returns one of
   192  ** PERM_REG, PERM_EXE, or PERM_LNK.
   193  */
   194  static int mfile_permstr_int(const char *zPerm){
   195    if(!zPerm || !*zPerm) return PERM_REG;
   196    else if(strstr(zPerm,"x")) return PERM_EXE;
   197    else if(strstr(zPerm,"l")) return PERM_LNK;
   198    else return PERM_REG/*???*/;
   199  }
   200  
   201  /*
   202  ** Internal helper for checkin_mini() and friends. Appends an F-card
   203  ** for p to pOut.
   204  */
   205  static void checkin_mini_append_fcard(Blob *pOut,
   206                                        const ManifestFile *p){
   207    if(p->zUuid){
   208      assert(*p->zUuid);
   209      blob_appendf(pOut, "F %F %s%s", p->zName,
   210                   p->zUuid,
   211                   mfile_permint_mstring(manifest_file_mperm(p)));
   212      if(p->zPrior){
   213        assert(*p->zPrior);
   214        blob_appendf(pOut, " %F\n", p->zPrior);
   215      }else{
   216        blob_append(pOut, "\n", 1);
   217      }
   218    }else{
   219      /* File was removed from parent delta. */
   220      blob_appendf(pOut, "F %F\n", p->zName);
   221    }
   222  }
   223  
   224  /*
   225  ** Handles the F-card parts for create_manifest_mini().
   226  **
   227  ** If asDelta is true, F-cards will be handled as for a delta
   228  ** manifest, and the caller MUST have added a B-card to pOut before
   229  ** calling this.
   230  **
   231  ** Returns 1 on success, 0 on error, and writes any error message to
   232  ** pErr (if it's not NULL). The only non-immediately-fatal/panic error
   233  ** is if pCI->filePerm is PERM_LNK or pCI would update a PERM_LNK
   234  ** in-repo file.
   235  */
   236  static int create_manifest_mini_fcards( Blob * pOut,
   237                                          CheckinMiniInfo * pCI,
   238                                          int asDelta,
   239                                          Blob * pErr){
   240    int wroteThisCard = 0;
   241    const ManifestFile * pFile;
   242    int (*fncmp)(char const *, char const *) =  /* filename comparator */
   243      filenames_are_case_sensitive()
   244      ? fossil_strcmp
   245      : fossil_stricmp;
   246  #define mf_err(EXPR) if(pErr) blob_appendf EXPR; return 0
   247  #define write_this_card(NAME) \
   248    blob_appendf(pOut, "F %F %b%s\n", (NAME), &pCI->fileHash, \
   249                 mfile_permint_mstring(pCI->filePerm)); \
   250    wroteThisCard = 1
   251  
   252    assert(pCI->filePerm!=PERM_LNK && "This should have been validated before.");
   253    assert(pCI->filePerm==PERM_REG || pCI->filePerm==PERM_EXE);
   254    if(PERM_LNK==pCI->filePerm){
   255      goto err_no_symlink;
   256    }
   257    manifest_file_rewind(pCI->pParent);
   258    if(asDelta!=0 && (pCI->pParent->zBaseline==0
   259                      || pCI->pParent->nFile==0)){
   260      /* Parent is a baseline or a delta with no F-cards, so this is
   261      ** the simplest case: create a delta with a single F-card.
   262      */
   263      pFile = manifest_file_find(pCI->pParent, pCI->zFilename);
   264      if(pFile!=0 && manifest_file_mperm(pFile)==PERM_LNK){
   265        goto err_no_symlink;
   266      }
   267      write_this_card(pFile ? pFile->zName : pCI->zFilename);
   268      return 1;
   269    }
   270    while(1){
   271      int cmp;
   272      if(asDelta==0){
   273        pFile = manifest_file_next(pCI->pParent, 0);
   274      }else{
   275        /* Parent is a delta manifest with F-cards. Traversal of delta
   276        ** manifest file entries is normally done via
   277        ** manifest_file_next(), which takes into account the
   278        ** differences between the delta and its parent and returns
   279        ** F-cards from both. Each successive delta from the same
   280        ** baseline includes all F-card changes from the previous
   281        ** deltas, so we instead clone the parent's F-cards except for
   282        ** the one (if any) which matches the new file.
   283        */
   284        pFile = pCI->pParent->iFile < pCI->pParent->nFile
   285          ? &pCI->pParent->aFile[pCI->pParent->iFile++]
   286          : 0;
   287      }
   288      if(0==pFile) break;
   289      cmp = fncmp(pFile->zName, pCI->zFilename);
   290      if(cmp<0){
   291        checkin_mini_append_fcard(pOut,pFile);
   292      }else{
   293        if(cmp==0 || 0==wroteThisCard){
   294          assert(0==wroteThisCard);
   295          if(PERM_LNK==manifest_file_mperm(pFile)){
   296            goto err_no_symlink;
   297          }
   298          write_this_card(cmp==0 ? pFile->zName : pCI->zFilename);
   299        }
   300        if(cmp>0){
   301          assert(wroteThisCard!=0);
   302          checkin_mini_append_fcard(pOut,pFile);
   303        }
   304      }
   305    }
   306    if(wroteThisCard==0){
   307      write_this_card(pCI->zFilename);
   308    }
   309    return 1;
   310  err_no_symlink:
   311    mf_err((pErr,"Cannot commit or overwrite symlinks "
   312            "via mini-checkin."));
   313    return 0;
   314  #undef write_this_card
   315  #undef mf_err
   316  }
   317  
   318  /*
   319  ** Creates a manifest file, written to pOut, from the state in the
   320  ** fully-populated and semantically valid pCI argument. pCI is not
   321  ** *semantically* modified by this routine but cannot be const because
   322  ** blob_str() may need to NUL-terminate any given blob.
   323  **
   324  ** Returns true on success. On error, returns 0 and, if pErr is not
   325  ** NULL, writes an error message there.
   326  **
   327  ** Intended only to be called via checkin_mini() or routines which
   328  ** have already completely vetted pCI for semantic validity.
   329  */
   330  static int create_manifest_mini( Blob * pOut, CheckinMiniInfo * pCI,
   331                                   Blob * pErr){
   332    Blob zCard = empty_blob;     /* Z-card checksum */
   333    int asDelta = 0;
   334  #define mf_err(EXPR) if(pErr) blob_appendf EXPR; return 0
   335  
   336    assert(blob_str(&pCI->fileHash));
   337    assert(pCI->pParent);
   338    assert(pCI->zFilename);
   339    assert(pCI->zUser);
   340    assert(pCI->zDate);
   341  
   342    /* Potential TODOs include...
   343    **
   344    ** - Maybe add support for tags. Those can be edited via /info page,
   345    **   and feel like YAGNI/feature creep for this purpose.
   346    */
   347    blob_zero(pOut);
   348    manifest_file_rewind(pCI->pParent) /* force load of baseline */;
   349    /* Determine whether we want to create a delta manifest... */
   350    if((CIMINI_PREFER_DELTA & pCI->flags)
   351       && ((CIMINI_STRONGLY_PREFER_DELTA & pCI->flags)
   352           || (pCI->pParent->pBaseline
   353               ? pCI->pParent->pBaseline
   354               : pCI->pParent)->nFile > 15
   355           /* 15 is arbitrary: don't create a delta when there is only a
   356           ** tiny gain for doing so. That heuristic is not *quite*
   357           ** right, in that when we're deriving from another delta, we
   358           ** really should compare the F-card count between it and its
   359           ** baseline, and create a delta if the baseline has (say)
   360           ** twice or more as many F-cards as the previous delta. */)
   361       && !db_get_boolean("forbid-delta-manifests",0)
   362       ){
   363      asDelta = 1;
   364      blob_appendf(pOut, "B %s\n",
   365                   pCI->pParent->zBaseline
   366                   ? pCI->pParent->zBaseline
   367                   : pCI->zParentUuid);
   368    }
   369    if(blob_size(&pCI->comment)!=0){
   370      blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment));
   371    }else{
   372      blob_append(pOut, "C (no\\scomment)\n", 16);
   373    }
   374    blob_appendf(pOut, "D %s\n", pCI->zDate);
   375    if(create_manifest_mini_fcards(pOut,pCI,asDelta,pErr)==0){
   376      return 0;
   377    }
   378    if(pCI->zCommentMimetype!=0 && pCI->zCommentMimetype[0]!=0){
   379      blob_appendf(pOut, "N %F\n", pCI->zCommentMimetype);
   380    }
   381    blob_appendf(pOut, "P %s\n", pCI->zParentUuid);
   382    blob_appendf(pOut, "U %F\n", pCI->zUser);
   383    md5sum_blob(pOut, &zCard);
   384    blob_appendf(pOut, "Z %b\n", &zCard);
   385    blob_reset(&zCard);
   386    return 1;
   387  #undef mf_err
   388  }
   389  
   390  /*
   391  ** A so-called "single-file/mini/web checkin" is a slimmed-down form
   392  ** of the checkin command which accepts only a single file and is
   393  ** intended to accept edits to a file via the web interface or from
   394  ** the CLI from outside of a checkout.
   395  **
   396  ** Being fully non-interactive is a requirement for this function,
   397  ** thus it cannot perform autosync or similar activities (which
   398  ** includes checking for repo locks).
   399  **
   400  ** This routine uses the state from the given fully-populated pCI
   401  ** argument to add pCI->fileContent to the database, and create and
   402  ** save a manifest for that change. Ownership of pCI and its contents
   403  ** are unchanged.
   404  **
   405  ** This function may may modify pCI as follows:
   406  **
   407  ** - If one of Manifest pCI->pParent or pCI->zParentUuid are NULL,
   408  **   then the other will be assigned based on its counterpart. Both
   409  **   may not be NULL.
   410  **
   411  ** - pCI->zDate is normalized to/replaced with a valid date/time
   412  **   string. If its original value cannot be validated then
   413  **   this function fails. If pCI->zDate is NULL, the current time
   414  **   is used.
   415  **
   416  ** - If the CIMINI_CONVERT_EOL_INHERIT flag is set,
   417  **   pCI->fileContent appears to be plain text, and its line-ending
   418  **   style differs from its previous version, it is converted to the
   419  **   same EOL style as the previous version. If this is done, the
   420  **   pCI->fileHash is re-computed. Note that only pCI->fileContent,
   421  **   not the original file, is affected by the conversion.
   422  **
   423  ** - Else if one of the CIMINI_CONVERT_EOL_WINDOWS or
   424  **   CIMINI_CONVERT_EOL_UNIX flags are set, pCI->fileContent is
   425  **   converted, if needed, to the corresponding EOL style.
   426  **
   427  ** - If EOL conversion takes place, pCI->fileHash is re-calculated.
   428  **
   429  ** - If pCI->fileHash is empty, this routine populates it with the
   430  **   repository's preferred hash algorithm (after any EOL conversion).
   431  **
   432  ** - pCI->comment may be converted to Unix-style newlines.
   433  **
   434  ** pCI's ownership is not modified.
   435  **
   436  ** This function validates pCI's state and fails if any validation
   437  ** fails.
   438  **
   439  ** On error, returns false (0) and, if pErr is not NULL, writes a
   440  ** diagnostic message there.
   441  ** 
   442  ** Returns true on success. If pRid is not NULL, the RID of the
   443  ** resulting manifest is written to *pRid.
   444  **
   445  ** The checkin process is largely influenced by pCI->flags, and that
   446  ** must be populated before calling this. See the fossil_cimini_flags
   447  ** enum for the docs for each flag.
   448  */
   449  static int checkin_mini(CheckinMiniInfo * pCI, int *pRid, Blob * pErr){
   450    Blob mf = empty_blob;             /* output manifest */
   451    int rid = 0, frid = 0;            /* various RIDs */
   452    int isPrivate;                    /* whether this is private content
   453                                         or not */
   454    ManifestFile * zFilePrev;         /* file entry from pCI->pParent */
   455    int prevFRid = 0;                 /* RID of file's prev. version */
   456  #define ci_err(EXPR) if(pErr!=0){blob_appendf EXPR;} goto ci_error
   457  
   458    db_begin_transaction();
   459    if(pCI->pParent==0 && pCI->zParentUuid==0){
   460      ci_err((pErr, "Cannot determine parent version."));
   461    }
   462    else if(pCI->pParent==0){
   463      pCI->pParent = manifest_get_by_name(pCI->zParentUuid, 0);
   464      if(pCI->pParent==0){
   465        ci_err((pErr,"Cannot load manifest for [%S].", pCI->zParentUuid));
   466      }
   467    }else if(pCI->zParentUuid==0){
   468      pCI->zParentUuid = rid_to_uuid(pCI->pParent->rid);
   469      assert(pCI->zParentUuid);
   470    }
   471    assert(pCI->pParent->rid>0);
   472    if(leaf_is_closed(pCI->pParent->rid)){
   473      ci_err((pErr,"Cannot commit to a closed leaf."));
   474      /* Remember that in order to override this we'd also need to
   475      ** cancel TAG_CLOSED on pCI->pParent. There would seem to be no
   476      ** reason we can't do that via the generated manifest, but the
   477      ** commit command does not offer that option, so mini-checkin
   478      ** probably shouldn't, either.
   479      */
   480    }
   481    if( !db_exists("SELECT 1 FROM user WHERE login=%Q", pCI->zUser) ){
   482      ci_err((pErr,"No such user: %s", pCI->zUser));
   483    }
   484    if(!(CIMINI_ALLOW_FORK & pCI->flags)
   485       && !is_a_leaf(pCI->pParent->rid)){
   486      ci_err((pErr,"Parent [%S] is not a leaf and forking is disabled.",
   487              pCI->zParentUuid));
   488    }
   489    if(!(CIMINI_ALLOW_MERGE_MARKER & pCI->flags)
   490       && contains_merge_marker(&pCI->fileContent)){
   491      ci_err((pErr,"Content appears to contain a merge conflict marker."));
   492    }
   493    if(!file_is_simple_pathname(pCI->zFilename, 1)){
   494      ci_err((pErr,"Invalid filename for use in a repository: %s",
   495              pCI->zFilename));
   496    }
   497    if(!(CIMINI_ALLOW_OLDER & pCI->flags)
   498       && !checkin_is_younger(pCI->pParent->rid, pCI->zDate)){
   499      ci_err((pErr,"Checkin time (%s) may not be older "
   500              "than its parent (%z).",
   501              pCI->zDate,
   502              db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%f',%lf)",
   503                      pCI->pParent->rDate)
   504              ));
   505    }
   506    {
   507      /*
   508      ** Normalize the timestamp. We don't use date_in_standard_format()
   509      ** because that has side-effects we don't want to trigger here.
   510      */
   511      char * zDVal = db_text(
   512           0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%f',%Q)",
   513           pCI->zDate ? pCI->zDate : "now");
   514      if(zDVal==0 || zDVal[0]==0){
   515        fossil_free(zDVal);
   516        ci_err((pErr,"Invalid timestamp string: %s", pCI->zDate));
   517      }
   518      fossil_free(pCI->zDate);
   519      pCI->zDate = zDVal;
   520    }
   521    { /* Confirm that only one EOL policy is in place. */
   522      int n = 0;
   523      if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags) ++n;
   524      if(CIMINI_CONVERT_EOL_UNIX & pCI->flags) ++n;
   525      if(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags) ++n;
   526      if(n>1){
   527        ci_err((pErr,"More than 1 EOL conversion policy was specified."));
   528      }
   529    }
   530    /* Potential TODOs include:
   531    **
   532    ** - Commit allows an empty checkin only with a flag, but we
   533    **   currently disallow an empty checkin entirely. Conform with
   534    **   commit?
   535    **
   536    ** Non-TODOs:
   537    **
   538    ** - Check for a commit lock would require auto-sync, which this
   539    **   code cannot do if it's going to be run via a web page.
   540    */
   541  
   542    /*
   543    ** Confirm that pCI->zFilename can be found in pCI->pParent.  If
   544    ** not, fail unless the CIMINI_ALLOW_NEW_FILE flag is set. This is
   545    ** admittedly an artificial limitation, not strictly necessary. We
   546    ** do it to hopefully reduce the chance of an "oops" where file
   547    ** X/Y/z gets committed as X/Y/Z or X/y/z due to a typo or
   548    ** case-sensitivity mismatch between the user/repo/filesystem, or
   549    ** some such.
   550    */
   551    manifest_file_rewind(pCI->pParent);
   552    zFilePrev = manifest_file_find(pCI->pParent, pCI->zFilename);
   553    if(!(CIMINI_ALLOW_NEW_FILE & pCI->flags)
   554       && (!zFilePrev
   555           || !zFilePrev->zUuid/*was removed from parent delta manifest*/)
   556       ){
   557      ci_err((pErr,"File [%s] not found in manifest [%S]. "
   558              "Adding new files is currently not permitted.",
   559              pCI->zFilename, pCI->zParentUuid));
   560    }else if(zFilePrev
   561             && manifest_file_mperm(zFilePrev)==PERM_LNK){
   562      ci_err((pErr,"Cannot save a symlink via a mini-checkin."));
   563    }
   564    if(zFilePrev){
   565      prevFRid = fast_uuid_to_rid(zFilePrev->zUuid);
   566    }
   567  
   568    if(((CIMINI_CONVERT_EOL_INHERIT & pCI->flags)
   569        || (CIMINI_CONVERT_EOL_UNIX & pCI->flags)
   570        || (CIMINI_CONVERT_EOL_WINDOWS & pCI->flags))
   571       && blob_size(&pCI->fileContent)>0
   572       ){
   573      /* Convert to the requested EOL style. Note that this inherently
   574      ** runs a risk of breaking content, e.g. string literals which
   575      ** contain embedded newlines. Note that HTML5 specifies that
   576      ** form-submitted TEXTAREA content gets normalized to CRLF-style:
   577      **
   578      ** https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element
   579      */
   580      const int pseudoBinary = LOOK_LONG | LOOK_NUL;
   581      const int lookFlags = LOOK_CRLF | LOOK_LONE_LF | pseudoBinary;
   582      const int lookNew = looks_like_utf8( &pCI->fileContent, lookFlags );
   583      if(!(pseudoBinary & lookNew)){
   584        int rehash = 0;
   585        /*fossil_print("lookNew=%08x\n",lookNew);*/
   586        if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags){
   587          Blob contentPrev = empty_blob;
   588          int lookOrig, nOrig;
   589          content_get(prevFRid, &contentPrev);
   590          lookOrig = looks_like_utf8(&contentPrev, lookFlags);
   591          nOrig = blob_size(&contentPrev);
   592          blob_reset(&contentPrev);
   593          /*fossil_print("lookOrig=%08x\n",lookOrig);*/
   594          if(nOrig>0 && lookOrig!=lookNew){
   595            /* If there is a newline-style mismatch, adjust the new
   596            ** content version to the previous style, then re-hash the
   597            ** content. Note that this means that what we insert is NOT
   598            ** what's in the filesystem.
   599            */
   600            if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){
   601              /* Old has Unix-style, new has Windows-style. */
   602              blob_to_lf_only(&pCI->fileContent);
   603              rehash = 1;
   604            }else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){
   605              /* Old has Windows-style, new has Unix-style. */
   606              blob_add_cr(&pCI->fileContent);
   607              rehash = 1;
   608            }
   609          }
   610        }else{
   611          const int oldSize = blob_size(&pCI->fileContent);
   612          if(CIMINI_CONVERT_EOL_UNIX & pCI->flags){
   613            if(LOOK_CRLF & lookNew){
   614              blob_to_lf_only(&pCI->fileContent);
   615            }
   616          }else{
   617            assert(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags);
   618            if(!(LOOK_CRLF & lookNew)){
   619              blob_add_cr(&pCI->fileContent);
   620            }
   621          }
   622          if(blob_size(&pCI->fileContent)!=oldSize){
   623            rehash = 1;
   624          }
   625        }
   626        if(rehash!=0){
   627          hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
   628        }
   629      }
   630    }/* end EOL conversion */
   631  
   632    if(blob_size(&pCI->fileHash)==0){
   633      /* Hash the content if it's not done already... */
   634      hname_hash(&pCI->fileContent, 0, &pCI->fileHash);
   635      assert(blob_size(&pCI->fileHash)>0);
   636    }
   637    if(zFilePrev){
   638      /* Has this file been changed since its previous commit?  Note
   639      ** that we have to delay this check until after the potentially
   640      ** expensive EOL conversion. */
   641      assert(blob_size(&pCI->fileHash));
   642      if(0==fossil_strcmp(zFilePrev->zUuid, blob_str(&pCI->fileHash))
   643         && manifest_file_mperm(zFilePrev)==pCI->filePerm){
   644        ci_err((pErr,"File is unchanged. Not committing."));
   645      }
   646    }
   647  #if 1
   648    /* Do we really want to normalize comment EOLs? Web-posting will
   649    ** submit them in CRLF or LF format, depending on how exactly the
   650    ** content is submitted (FORM (CRLF) or textarea-to-POST (LF, at
   651    ** least in theory)). */
   652    blob_to_lf_only(&pCI->comment);
   653  #endif
   654    /* Create, save, deltify, and crosslink the manifest... */
   655    if(create_manifest_mini(&mf, pCI, pErr)==0){
   656      return 0;
   657    }
   658    isPrivate = content_is_private(pCI->pParent->rid);
   659    rid = content_put_ex(&mf, 0, 0, 0, isPrivate);
   660    if(pCI->flags & CIMINI_DUMP_MANIFEST){
   661      fossil_print("%b", &mf);
   662    }
   663    if(pCI->pMfOut!=0){
   664      /* Cross-linking clears mf, so we have to copy it,
   665      ** instead of taking over its memory. */
   666      blob_reset(pCI->pMfOut);
   667      blob_append(pCI->pMfOut, blob_buffer(&mf), blob_size(&mf));
   668    }
   669    content_deltify(rid, &pCI->pParent->rid, 1, 0);
   670    manifest_crosslink(rid, &mf, 0);
   671    blob_reset(&mf);
   672    /* Save and deltify the file content... */
   673    frid = content_put_ex(&pCI->fileContent, blob_str(&pCI->fileHash),
   674                          0, 0, isPrivate);
   675    if(zFilePrev!=0){
   676      assert(prevFRid>0);
   677      content_deltify(frid, &prevFRid, 1, 0);
   678    }
   679    db_end_transaction((CIMINI_DRY_RUN & pCI->flags) ? 1 : 0);
   680    if(pRid!=0){
   681      *pRid = rid;
   682    }
   683    return 1;
   684  ci_error:
   685    assert(db_transaction_nesting_depth()>0);
   686    db_end_transaction(1);
   687    return 0;
   688  #undef ci_err
   689  }
   690  
   691  /*
   692  ** COMMAND: test-ci-mini
   693  **
   694  ** This is an on-going experiment, subject to change or removal at
   695  ** any time.
   696  **
   697  ** Usage: %fossil test-ci-mini ?OPTIONS? FILENAME
   698  **
   699  ** where FILENAME is a repo-relative name as it would appear in the
   700  ** vfile table.
   701  **
   702  ** Options:
   703  **
   704  **   --repository|-R REPO      The repository file to commit to.
   705  **   --as FILENAME             The repository-side name of the input
   706  **                             file, relative to the top of the
   707  **                             repository. Default is the same as the
   708  **                             input file name.
   709  **   --comment|-m COMMENT      Required checkin comment.
   710  **   --comment-file|-M FILE    Reads checkin comment from the given file.
   711  **   --revision|-r VERSION     Commit from this version. Default is
   712  **                             the checkout version (if available) or
   713  **                             trunk (if used without a checkout).
   714  **   --allow-fork              Allows the commit to be made against a
   715  **                             non-leaf parent. Note that no autosync
   716  **                             is performed beforehand.
   717  **   --allow-merge-conflict    Allows checkin of a file even if it
   718  **                             appears to contain a fossil merge conflict
   719  **                             marker.
   720  **   --user-override USER      USER to use instead of the current
   721  **                             default.
   722  **   --date-override DATETIME  DATE to use instead of 'now'.
   723  **   --allow-older             Allow a commit to be older than its
   724  **                             ancestor.
   725  **   --convert-eol-inherit     Convert EOL style of the checkin to match
   726  **                             the previous version's content.
   727  **   --convert-eol-unix        Convert the EOL style to Unix.
   728  **   --convert-eol-windows     Convert the EOL style to Windows.
   729  **   (only one of the --convert-eol-X options may be used and they only
   730  **    modified the saved blob, not the input file.)
   731  **   --delta                   Prefer to generate a delta manifest, if
   732  **                             able. The forbid-delta-manifests repo
   733  **                             config option trumps this, as do certain
   734  **                             heuristics.
   735  **   --allow-new-file          Allow addition of a new file this way.
   736  **                             Disabled by default to avoid that case-
   737  **                             sensitivity errors inadvertently lead to
   738  **                             adding a new file where an update is
   739  **                             intended.
   740  **   --dump-manifest|-d        Dumps the generated manifest to stdout
   741  **                             immediately after it's generated.
   742  **   --save-manifest FILE      Saves the generated manifest to a file
   743  **                             after successfully processing it.
   744  **   --wet-run                 Disables the default dry-run mode.
   745  **
   746  ** Example:
   747  **
   748  ** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c
   749  **
   750  */
   751  void test_ci_mini_cmd(void){
   752    CheckinMiniInfo cimi;       /* checkin state */
   753    int newRid = 0;                /* RID of new version */
   754    const char * zFilename;        /* argv[2] */
   755    const char * zComment;         /* -m comment */
   756    const char * zCommentFile;     /* -M FILE */
   757    const char * zAsFilename;      /* --as filename */
   758    const char * zRevision;        /* --revision|-r [=trunk|checkout] */
   759    const char * zUser;            /* --user-override */
   760    const char * zDate;            /* --date-override */
   761    char const * zManifestFile = 0;/* --save-manifest FILE */
   762  
   763    /* This function should perform only the minimal "business logic" it
   764    ** needs in order to fully/properly populate the CheckinMiniInfo and
   765    ** then pass it on to checkin_mini() to do most of the validation
   766    ** and work. The point of this is to avoid duplicate code when a web
   767    ** front-end is added for checkin_mini().
   768    */
   769    CheckinMiniInfo_init(&cimi);
   770    zComment = find_option("comment","m",1);
   771    zCommentFile = find_option("comment-file","M",1);
   772    zAsFilename = find_option("as",0,1);
   773    zRevision = find_option("revision","r",1);
   774    zUser = find_option("user-override",0,1);
   775    zDate = find_option("date-override",0,1);
   776    zManifestFile = find_option("save-manifest",0,1);
   777    if(find_option("wet-run",0,0)==0){
   778      cimi.flags |= CIMINI_DRY_RUN;
   779    }
   780    if(find_option("allow-fork",0,0)!=0){
   781      cimi.flags |= CIMINI_ALLOW_FORK;
   782    }
   783    if(find_option("dump-manifest","d",0)!=0){
   784      cimi.flags |= CIMINI_DUMP_MANIFEST;
   785    }
   786    if(find_option("allow-merge-conflict",0,0)!=0){
   787      cimi.flags |= CIMINI_ALLOW_MERGE_MARKER;
   788    }
   789    if(find_option("allow-older",0,0)!=0){
   790      cimi.flags |= CIMINI_ALLOW_OLDER;
   791    }
   792    if(find_option("convert-eol-inherit",0,0)!=0){
   793      cimi.flags |= CIMINI_CONVERT_EOL_INHERIT;
   794    }else if(find_option("convert-eol-unix",0,0)!=0){
   795      cimi.flags |= CIMINI_CONVERT_EOL_UNIX;
   796    }else if(find_option("convert-eol-windows",0,0)!=0){
   797      cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS;
   798    }
   799    if(find_option("delta",0,0)!=0){
   800      cimi.flags |= CIMINI_PREFER_DELTA;
   801    }
   802    if(find_option("delta2",0,0)!=0){
   803      /* Undocumented. For testing only. */
   804      cimi.flags |= CIMINI_PREFER_DELTA | CIMINI_STRONGLY_PREFER_DELTA;
   805    }
   806    if(find_option("allow-new-file",0,0)!=0){
   807      cimi.flags |= CIMINI_ALLOW_NEW_FILE;
   808    }
   809    db_find_and_open_repository(0, 0);
   810    verify_all_options();
   811    user_select();
   812    if(g.argc!=3){
   813      usage("INFILE");
   814    }
   815    if(zComment && zCommentFile){
   816      fossil_fatal("Only one of -m or -M, not both, may be used.");
   817    }else{
   818      if(zCommentFile && *zCommentFile){
   819        blob_read_from_file(&cimi.comment, zCommentFile, ExtFILE);
   820      }else if(zComment && *zComment){
   821        blob_append(&cimi.comment, zComment, -1);
   822      }
   823      if(!blob_size(&cimi.comment)){
   824        fossil_fatal("Non-empty checkin comment is required.");
   825      }
   826    }
   827    db_begin_transaction();
   828    zFilename = g.argv[2];
   829    cimi.zFilename = mprintf("%/", zAsFilename ? zAsFilename : zFilename);
   830    cimi.filePerm = file_perm(zFilename, ExtFILE);
   831    cimi.zUser = mprintf("%s", zUser ? zUser : login_name());
   832    if(zDate){
   833      cimi.zDate = mprintf("%s", zDate);
   834    }
   835    if(zRevision==0 || zRevision[0]==0){
   836      if(g.localOpen/*checkout*/){
   837        zRevision = db_lget("checkout-hash", 0)/*leak*/;
   838      }else{
   839        zRevision = "trunk";
   840      }
   841    }
   842    name_to_uuid2(zRevision, "ci", &cimi.zParentUuid);
   843    if(cimi.zParentUuid==0){
   844      fossil_fatal("Cannot determine version to commit to.");
   845    }
   846    blob_read_from_file(&cimi.fileContent, zFilename, ExtFILE);
   847    {
   848      Blob theManifest = empty_blob; /* --save-manifest target */
   849      Blob errMsg = empty_blob;
   850      int rc;
   851      if(zManifestFile){
   852        cimi.pMfOut = &theManifest;
   853      }
   854      rc = checkin_mini(&cimi, &newRid, &errMsg);
   855      if(rc){
   856        assert(blob_size(&errMsg)==0);
   857      }else{
   858        assert(blob_size(&errMsg));
   859        fossil_fatal("%b", &errMsg);
   860      }
   861      if(zManifestFile){
   862        fossil_print("Writing manifest to: %s\n", zManifestFile);
   863        assert(blob_size(&theManifest)>0);
   864        blob_write_to_file(&theManifest, zManifestFile);
   865        blob_reset(&theManifest);
   866      }
   867    }
   868    if(newRid!=0){
   869      fossil_print("New version%s: %z\n",
   870                   (cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "",
   871                   rid_to_uuid(newRid));
   872    }
   873    db_end_transaction(0/*checkin_mini() will have triggered it to roll
   874                        ** back in dry-run mode, but we need access to
   875                        ** the transaction-written db state in this
   876                        ** routine.*/);
   877    if(!(cimi.flags & CIMINI_DRY_RUN) && newRid!=0 && g.localOpen!=0){
   878      fossil_warning("The checkout state is now out of sync "
   879                     "with regards to this commit. It needs to be "
   880                     "'update'd or 'close'd and re-'open'ed.");
   881    }
   882    CheckinMiniInfo_cleanup(&cimi);
   883  }
   884  
   885  /*
   886  ** If the fileedit-glob setting has a value, this returns its Glob
   887  ** object (in memory owned by this function), else it returns NULL.
   888  */
   889  Glob *fileedit_glob(void){
   890    static Glob * pGlobs = 0;
   891    static int once = 0;
   892    if(0==pGlobs && once==0){
   893      char * zGlobs = db_get("fileedit-glob",0);
   894      once = 1;
   895      if(0!=zGlobs && 0!=*zGlobs){
   896        pGlobs = glob_create(zGlobs);
   897      }
   898      fossil_free(zGlobs);
   899    }
   900    return pGlobs;
   901  }
   902  
   903  /*
   904  ** Returns true if the given filename qualifies for online editing by
   905  ** the current user, else returns false.
   906  **
   907  ** Editing requires that the user have the Write permission and that
   908  ** the filename match the glob defined by the fileedit-glob setting.
   909  ** A missing or empty value for that glob disables all editing.
   910  */
   911  int fileedit_is_editable(const char *zFilename){
   912    Glob * pGlobs = fileedit_glob();
   913    if(pGlobs!=0 && zFilename!=0 && *zFilename!=0 && 0!=g.perm.Write){
   914      return glob_match(pGlobs, zFilename);
   915    }else{
   916      return 0;
   917    }
   918  }
   919  
   920  /*
   921  ** Given a repo-relative filename and a manifest RID, returns the UUID
   922  ** of the corresponding file entry.  Returns NULL if no match is
   923  ** found.  If pFilePerm is not NULL, the file's permission flag value
   924  ** is written to *pFilePerm.
   925  */
   926  static char *fileedit_file_uuid(char const *zFilename,
   927                                  int vid, int *pFilePerm){
   928    Stmt stmt = empty_Stmt;
   929    char * zFileUuid = 0;
   930    db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin "
   931               "WHERE filename=%Q %s AND checkinID=%d",
   932               zFilename, filename_collation(), vid);
   933    if(SQLITE_ROW==db_step(&stmt)){
   934      zFileUuid = mprintf("%s",db_column_text(&stmt, 0));
   935      if(pFilePerm){
   936        *pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1));
   937      }
   938    }
   939    db_finalize(&stmt);
   940    return zFileUuid;
   941  }
   942  
   943  /*
   944  ** Returns true if the current user is allowed to edit the given
   945  ** filename, as determined by fileedit_is_editable(), else false,
   946  ** in which case it queues up an error response and the caller
   947  ** must return immediately.
   948  */
   949  static int fileedit_ajax_check_filename(const char * zFilename){
   950    if(0==fileedit_is_editable(zFilename)){
   951      ajax_route_error(403, "File is disallowed by the "
   952                       "fileedit-glob setting.");
   953      return 0;
   954    }
   955    return 1;
   956  }
   957  
   958  /*
   959  ** Passed the values of the "checkin" and "filename" request
   960  ** properties, this function verifies that they are valid and
   961  ** populates:
   962  **
   963  ** - *zRevUuid = the fully-expanded value of zRev (owned by the
   964  **    caller). zRevUuid may be NULL.
   965  **
   966  ** - *pVid = the RID of zRevUuid. pVid May be NULL. If the vid
   967  **    cannot be resolved or is ambiguous, pVid is not assigned.
   968  **
   969  ** - *frid = the RID of zFilename's blob content. May not be NULL
   970  **   unless zFilename is also NULL. If BOTH of zFilename and frid are
   971  **   NULL then no confirmation is done on the filename argument - only
   972  **   zRev is checked.
   973  **
   974  ** Returns 0 if the given file is not in the given checkin or if
   975  ** fileedit_ajax_check_filename() fails, else returns true.  If it
   976  ** returns false, it queues up an error response and the caller must
   977  ** return immediately.
   978  */
   979  static int fileedit_ajax_setup_filerev(const char * zRev,
   980                                         char ** zRevUuid,
   981                                         int * pVid,
   982                                         const char * zFilename,
   983                                         int * frid){
   984    char * zFileUuid = 0;             /* file content UUID */
   985    const int checkFile = zFilename!=0 || frid!=0;
   986    int vid = 0;
   987    
   988    if(checkFile && !fileedit_ajax_check_filename(zFilename)){
   989      return 0;
   990    }
   991    vid = symbolic_name_to_rid(zRev, "ci");
   992    if(0==vid){
   993      ajax_route_error(404,"Cannot resolve name as a checkin: %s",
   994                       zRev);
   995      return 0;
   996    }else if(vid<0){
   997      ajax_route_error(400,"Checkin name is ambiguous: %s",
   998                       zRev);
   999      return 0;
  1000    }else if(pVid!=0){
  1001      *pVid = vid;
  1002    }
  1003    if(checkFile){
  1004      zFileUuid = fileedit_file_uuid(zFilename, vid, 0);
  1005      if(zFileUuid==0){
  1006        ajax_route_error(404, "Checkin does not contain file.");
  1007        return 0;
  1008      }
  1009    }
  1010    if(zRevUuid!=0){
  1011      *zRevUuid = rid_to_uuid(vid);
  1012    }
  1013    if(checkFile){
  1014      assert(zFileUuid!=0);
  1015      if(frid!=0){
  1016        *frid = fast_uuid_to_rid(zFileUuid);
  1017      }
  1018      fossil_free(zFileUuid);
  1019    }
  1020    return 1;
  1021  }
  1022  
  1023  /*
  1024  ** AJAX route /fileedit?ajax=content
  1025  **
  1026  ** Query parameters:
  1027  **
  1028  ** filename=FILENAME
  1029  ** checkin=CHECKIN_NAME
  1030  **
  1031  ** User must have Write access to use this page.
  1032  **
  1033  ** Responds with the raw content of the given page. On error it
  1034  ** produces a JSON response as documented for ajax_route_error().
  1035  **
  1036  ** Extra response headers:
  1037  **
  1038  ** x-fileedit-file-perm: empty or "x" or "l", representing PERM_REG,
  1039  ** PERM_EXE, or PERM_LINK, respectively.
  1040  **
  1041  ** x-fileedit-checkin-branch: branch name for the passed-in checkin.
  1042  */
  1043  static void fileedit_ajax_content(void){
  1044    const char * zFilename = 0;
  1045    const char * zRev = 0;
  1046    int vid, frid;
  1047    Blob content = empty_blob;
  1048    const char * zMime;
  1049  
  1050    ajax_get_fnci_args( &zFilename, &zRev );
  1051    if(!ajax_route_bootstrap(1,0)
  1052       || !fileedit_ajax_setup_filerev(zRev, 0, &vid,
  1053                                       zFilename, &frid)){
  1054      return;
  1055    }
  1056    zMime = mimetype_from_name(zFilename);
  1057    content_get(frid, &content);
  1058    if(0==zMime){
  1059      if(looks_like_binary(&content)){
  1060        zMime = "application/octet-stream";
  1061      }else{
  1062        zMime = "text/plain";
  1063      }
  1064    }
  1065    { /* Send the is-exec bit via response header so that the UI can be
  1066      ** updated to account for that. */
  1067      int fperm = 0;
  1068      char * zFuuid = fileedit_file_uuid(zFilename, vid, &fperm);
  1069      const char * zPerm = mfile_permint_mstring(fperm);
  1070      assert(zFuuid);
  1071      cgi_printf_header("x-fileedit-file-perm:%s\r\n", zPerm);
  1072      fossil_free(zFuuid);
  1073    }
  1074    { /* Send branch name via response header for UI usability reasons */
  1075      char * zBranch = branch_of_rid(vid);
  1076      if(zBranch!=0 && zBranch[0]!=0){
  1077        cgi_printf_header("x-fileedit-checkin-branch: %s\r\n", zBranch);
  1078      }
  1079      fossil_free(zBranch);
  1080    }
  1081    cgi_set_content_type(zMime);
  1082    cgi_set_content(&content);
  1083  }
  1084  
  1085  /*
  1086  ** AJAX route /fileedit?ajax=diff
  1087  **
  1088  ** Required query parameters:
  1089  **
  1090  ** filename=FILENAME
  1091  ** content=text
  1092  ** checkin=checkin version
  1093  **
  1094  ** Optional parameters:
  1095  **
  1096  ** sbs=integer (1=side-by-side or 0=unified, default=0)
  1097  **
  1098  ** ws=integer (0=diff whitespace, 1=ignore EOL ws, 2=ignore all ws)
  1099  **
  1100  ** Reminder to self: search info.c for isPatch to see how a
  1101  ** patch-style siff can be produced.
  1102  **
  1103  ** User must have Write access to use this page.
  1104  **
  1105  ** Responds with the HTML content of the diff. On error it produces a
  1106  ** JSON response as documented for ajax_route_error().
  1107  */
  1108  static void fileedit_ajax_diff(void){
  1109    /*
  1110    ** Reminder: we only need the filename to perform valdiation
  1111    ** against fileedit_is_editable(), else this route could be
  1112    ** abused to get diffs against content disallowed by the
  1113    ** whitelist.
  1114    */
  1115    const char * zFilename = 0;
  1116    const char * zRev = 0;
  1117    const char * zContent = P("content");
  1118    char * zRevUuid = 0;
  1119    int vid, frid, iFlag;
  1120    u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG;
  1121    Blob content = empty_blob;
  1122  
  1123    iFlag = atoi(PD("sbs","0"));
  1124    if(0==iFlag){
  1125      diffFlags |= DIFF_LINENO;
  1126    }else{
  1127      diffFlags |= DIFF_SIDEBYSIDE;
  1128    }
  1129    iFlag = atoi(PD("ws","2"));
  1130    if(2==iFlag){
  1131      diffFlags |= DIFF_IGNORE_ALLWS;
  1132    }else if(1==iFlag){
  1133      diffFlags |= DIFF_IGNORE_EOLWS;
  1134    }
  1135    diffFlags |= DIFF_STRIP_EOLCR;
  1136    ajax_get_fnci_args( &zFilename, &zRev );
  1137    if(!ajax_route_bootstrap(1,1)
  1138       || !fileedit_ajax_setup_filerev(zRev, &zRevUuid, &vid,
  1139                                       zFilename, &frid)){
  1140      return;
  1141    }
  1142    if(!zContent){
  1143      zContent = "";
  1144    }
  1145    cgi_set_content_type("text/html");
  1146    blob_init(&content, zContent, -1);
  1147    {
  1148      Blob orig = empty_blob;
  1149      content_get(frid, &orig);
  1150      ajax_render_diff(&orig, &content, diffFlags);
  1151      blob_reset(&orig);
  1152    }
  1153    fossil_free(zRevUuid);
  1154    blob_reset(&content);
  1155  }
  1156  
  1157  /*
  1158  ** Sets up and validates most, but not all, of p's checkin-related
  1159  ** state from the CGI environment. Returns 0 on success or a suggested
  1160  ** HTTP result code on error, in which case a message will have been
  1161  ** written to pErr.
  1162  **
1163 ** It always fails if it cannot completely resolve the 'file' and 'r'
1164 ** parameters, including verifying that the refer to a real 1165 ** file/version combination and editable by the current user. All 1166 ** others are optional (at this level, anyway, but upstream code might 1167 ** require them). 1168 ** 1169 ** If the 3rd argument is not NULL and an error is related to a 1170 ** missing arg then *bIsMissingArg is set to true. This is 1171 ** intended to allow /fileedit to squelch certain initialization 1172 ** errors. 1173 ** 1174 ** Intended to be used only by /filepage and /filepage_commit. 1175 */ 1176 static int fileedit_setup_cimi_from_p(CheckinMiniInfo * p, Blob * pErr, 1177 int * bIsMissingArg){ 1178 char * zFileUuid = 0; /* UUID of file content */ 1179 const char * zFlag; /* generic flag */ 1180 int rc = 0, vid = 0, frid = 0; /* result code, checkin/file rids */ 1181 1182 #define fail(EXPR) blob_appendf EXPR; goto end_fail 1183 zFlag = PD("filename",P("fn")); 1184 if(zFlag==0 || !*zFlag){ 1185 rc = 400; 1186 if(bIsMissingArg){ 1187 *bIsMissingArg = 1; 1188 } 1189 fail((pErr,"Missing required 'filename' parameter.")); 1190 } 1191 p->zFilename = mprintf("%s",zFlag); 1192 1193 if(0==fileedit_is_editable(p->zFilename)){ 1194 rc = 403; 1195 fail((pErr,"Filename [%h] is disallowed " 1196 "by the [fileedit-glob] repository " 1197 "setting.", 1198 p->zFilename)); 1199 } 1200 1201 zFlag = PD("checkin",P("ci")); 1202 if(!zFlag){ 1203 rc = 400; 1204 if(bIsMissingArg){ 1205 *bIsMissingArg = 1; 1206 } 1207 fail((pErr,"Missing required 'checkin' parameter.")); 1208 } 1209 vid = symbolic_name_to_rid(zFlag, "ci"); 1210 if(0==vid){ 1211 rc = 404; 1212 fail((pErr,"Could not resolve checkin version.")); 1213 }else if(vid<0){ 1214 rc = 400; 1215 fail((pErr,"Checkin name is ambiguous.")); 1216 } 1217 p->zParentUuid = rid_to_uuid(vid)/*fully expand it*/; 1218 1219 zFileUuid = fileedit_file_uuid(p->zFilename, vid, &p->filePerm); 1220 if(!zFileUuid){ 1221 rc = 404; 1222 fail((pErr,"Checkin [%S] does not contain file: " 1223 "[%h]", p->zParentUuid, p->zFilename)); 1224 }else if(PERM_LNK==p->filePerm){ 1225 rc = 400; 1226 fail((pErr,"Editing symlinks is not permitted.")); 1227 } 1228 1229 /* Find the repo-side file entry or fail... */ 1230 frid = fast_uuid_to_rid(zFileUuid); 1231 assert(frid); 1232 1233 /* Read file content from submit request or repo... */ 1234 zFlag = P("content"); 1235 if(zFlag==0){ 1236 content_get(frid, &p->fileContent); 1237 }else{ 1238 blob_init(&p->fileContent,zFlag,-1); 1239 } 1240 if(looks_like_binary(&p->fileContent)){ 1241 rc = 400; 1242 fail((pErr,"File appears to be binary. Cannot edit: " 1243 "[%h]",p->zFilename)); 1244 } 1245 1246 zFlag = PT("comment"); 1247 if(zFlag!=0 && *zFlag!=0){ 1248 blob_append(&p->comment, zFlag, -1); 1249 } 1250 zFlag = P("comment_mimetype"); 1251 if(zFlag){ 1252 p->zCommentMimetype = mprintf("%s",zFlag); 1253 zFlag = 0; 1254 } 1255 #define p_int(K) atoi(PD(K,"0")) 1256 if(p_int("dry_run")!=0){ 1257 p->flags |= CIMINI_DRY_RUN; 1258 } 1259 if(p_int("allow_fork")!=0){ 1260 p->flags |= CIMINI_ALLOW_FORK; 1261 } 1262 if(p_int("allow_older")!=0){ 1263 p->flags |= CIMINI_ALLOW_OLDER; 1264 } 1265 if(0==p_int("exec_bit")){ 1266 p->filePerm = PERM_REG; 1267 }else{ 1268 p->filePerm = PERM_EXE; 1269 } 1270 if(p_int("allow_merge_conflict")!=0){ 1271 p->flags |= CIMINI_ALLOW_MERGE_MARKER; 1272 } 1273 if(p_int("prefer_delta")!=0){ 1274 p->flags |= CIMINI_PREFER_DELTA; 1275 } 1276 1277 /* EOL conversion policy... */ 1278 switch(p_int("eol")){ 1279 case 1: p->flags |= CIMINI_CONVERT_EOL_UNIX; break; 1280 case 2: p->flags |= CIMINI_CONVERT_EOL_WINDOWS; break; 1281 default: p->flags |= CIMINI_CONVERT_EOL_INHERIT; break; 1282 } 1283 #undef p_int 1284 /* 1285 ** TODO?: date-override date selection field. Maybe use 1286 ** an input[type=datetime-local]. 1287 */ 1288 p->zUser = mprintf("%s",g.zLogin); 1289 return 0; 1290 end_fail: 1291 #undef fail 1292 fossil_free(zFileUuid); 1293 return rc ? rc : 500; 1294 } 1295 1296 /* 1297 ** Renders a list of all open leaves in JSON form: 1298 ** 1299 ** [ 1300 ** {checkin: UUID, branch: branchName, timestamp: string} 1301 ** ] 1302 ** 1303 ** The entries are ordered newest first. 1304 ** 1305 ** If zFirstUuid is not NULL then *zFirstUuid is set to a copy of the 1306 ** full UUID of the first (most recent) leaf, which must be freed by 1307 ** the caller. It is set to 0 if there are no leaves. 1308 */ 1309 static void fileedit_render_leaves_list(char ** zFirstUuid){ 1310 Blob sql = empty_blob; 1311 Stmt q = empty_Stmt; 1312 int i = 0; 1313 1314 if(zFirstUuid){ 1315 *zFirstUuid = 0; 1316 } 1317 blob_append(&sql, timeline_query_for_tty(), -1); 1318 blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " 1319 "WHERE NOT EXISTS(" 1320 "SELECT 1 from tagxref WHERE tagid=%d AND " 1321 "tagtype>0 AND rid=leaf.rid" 1322 ")) " 1323 "ORDER BY mtime DESC", TAG_CLOSED); 1324 db_prepare_blob(&q, &sql); 1325 CX("["); 1326 while( SQLITE_ROW==db_step(&q) ){ 1327 const char * zUuid = db_column_text(&q, 1); 1328 if(i++){ 1329 CX(","); 1330 }else if(zFirstUuid){ 1331 *zFirstUuid = fossil_strdup(zUuid); 1332 } 1333 CX("{"); 1334 CX("\"checkin\":%!j,", zUuid); 1335 CX("\"branch\":%!j,", db_column_text(&q, 7)); 1336 CX("\"timestamp\":%!j", db_column_text(&q, 2)); 1337 CX("}"); 1338 } 1339 CX("]"); 1340 db_finalize(&q); 1341 } 1342 1343 /* 1344 ** For the given fully resolved UUID, renders a JSON object containing 1345 ** the fileeedit-editable files in that checkin: 1346 ** 1347 ** { 1348 ** checkin: UUID, 1349 ** editableFiles: [ filename1, ... filenameN ] 1350 ** } 1351 ** 1352 ** They are sorted by name using filename_collation(). 1353 */ 1354 static void fileedit_render_checkin_files(const char * zFullUuid){ 1355 Blob sql = empty_blob; 1356 Stmt q = empty_Stmt; 1357 int i = 0; 1358 1359 CX("{\"checkin\":%!j," 1360 "\"editableFiles\":[", zFullUuid); 1361 blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " 1362 "ORDER BY filename %s", 1363 zFullUuid, filename_collation()); 1364 db_prepare_blob(&q, &sql); 1365 while( SQLITE_ROW==db_step(&q) ){ 1366 const char * zFilename = db_column_text(&q, 0); 1367 if(fileedit_is_editable(zFilename)){ 1368 if(i++){ 1369 CX(","); 1370 } 1371 CX("%!j", zFilename); 1372 } 1373 } 1374 db_finalize(&q); 1375 CX("]}"); 1376 } 1377 1378 /* 1379 ** AJAX route /fileedit?ajax=filelist 1380 ** 1381 ** Fetches a JSON-format list of leaves and/or filenames for use in 1382 ** creating a file selection list in /fileedit. It has different modes 1383 ** of operation depending on its arguments: 1384 ** 1385 ** 'leaves': just fetch a list of open leaf versions, in this 1386 ** format: 1387 ** 1388 ** [ 1389 ** {checkin: UUID, branch: branchName, timestamp: string} 1390 ** ] 1391 ** 1392 ** The entries are ordered newest first. 1393 ** 1394 ** 'checkin=CHECKIN_NAME': fetch the current list of is-editable files 1395 ** for the current user and given checkin name: 1396 ** 1397 ** { 1398 ** checkin: UUID, 1399 ** editableFiles: [ filename1, ... filenameN ] // sorted by name 1400 ** } 1401 ** 1402 ** On error it produces a JSON response as documented for 1403 ** ajax_route_error(). 1404 */ 1405 static void fileedit_ajax_filelist(){ 1406 const char * zCi = PD("checkin",P("ci")); 1407 1408 if(!ajax_route_bootstrap(1,0)){ 1409 return; 1410 } 1411 cgi_set_content_type("application/json"); 1412 if(zCi!=0){ 1413 char * zCiFull = 0; 1414 if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, 0, 0, 0)){ 1415 /* Error already reported */ 1416 return; 1417 } 1418 fileedit_render_checkin_files(zCiFull); 1419 fossil_free(zCiFull); 1420 }else if(P("leaves")!=0){ 1421 fileedit_render_leaves_list(0); 1422 }else{ 1423 ajax_route_error(500, "Unhandled URL argument."); 1424 } 1425 } 1426 1427 /* 1428 ** AJAX route /fileedit?ajax=commit 1429 ** 1430 ** Required query parameters: 1431 ** 1432 ** filename=FILENAME 1433 ** checkin=Parent checkin UUID 1434 ** content=text 1435 ** comment=non-empty text 1436 ** 1437 ** Optional query parameters: 1438 ** 1439 ** comment_mimetype=text (NOT currently honored) 1440 ** 1441 ** dry_run=int (1 or 0) 1442 ** 1443 ** include_manifest=int (1 or 0), whether to include 1444 ** the generated manifest in the response. 1445 ** 1446 ** 1447 ** User must have Write permissions to use this page. 1448 ** 1449 ** Responds with JSON (with some state repeated 1450 ** from the input in order to avoid certain race conditions 1451 ** client-side): 1452 ** 1453 ** { 1454 ** checkin: newUUID, 1455 ** filename: theFilename, 1456 ** mimetype: string, 1457 ** branch: name of the checkin's branch, 1458 ** isExe: bool, 1459 ** dryRun: bool, 1460 ** manifest: text of manifest, 1461 ** } 1462 ** 1463 ** On error it produces a JSON response as documented for 1464 ** ajax_route_error(). 1465 */ 1466 static void fileedit_ajax_commit(void){ 1467 Blob err = empty_blob; /* Error messages */ 1468 Blob manifest = empty_blob; /* raw new manifest */ 1469 CheckinMiniInfo cimi; /* checkin state */ 1470 int rc; /* generic result code */ 1471 int newVid = 0; /* new version's RID */ 1472 char * zNewUuid = 0; /* newVid's UUID */ 1473 char const * zMimetype; 1474 char * zBranch = 0; 1475 1476 if(!ajax_route_bootstrap(1,1)){ 1477 return; 1478 } 1479 db_begin_transaction(); 1480 CheckinMiniInfo_init(&cimi); 1481 rc = fileedit_setup_cimi_from_p(&cimi, &err, 0); 1482 if(0!=rc){ 1483 ajax_route_error(rc,"%b",&err); 1484 goto end_cleanup; 1485 } 1486 if(blob_size(&cimi.comment)==0){ 1487 ajax_route_error(400,"Empty checkin comment is not permitted."); 1488 goto end_cleanup; 1489 } 1490 if(0!=atoi(PD("include_manifest","0"))){ 1491 cimi.pMfOut = &manifest; 1492 } 1493 checkin_mini(&cimi, &newVid, &err); 1494 if(blob_size(&err)){ 1495 ajax_route_error(500,"%b",&err); 1496 goto end_cleanup; 1497 } 1498 assert(newVid>0); 1499 zNewUuid = rid_to_uuid(newVid); 1500 cgi_set_content_type("application/json"); 1501 CX("{"); 1502 CX("\"checkin\":%!j,", zNewUuid); 1503 CX("\"filename\":%!j,", cimi.zFilename); 1504 CX("\"isExe\": %s,", cimi.filePerm==PERM_EXE ? "true" : "false"); 1505 zMimetype = mimetype_from_name(cimi.zFilename); 1506 if(zMimetype!=0){ 1507 CX("\"mimetype\": %!j,", zMimetype); 1508 } 1509 zBranch = branch_of_rid(newVid); 1510 if(zBranch!=0){ 1511 CX("\"branch\": %!j,", zBranch); 1512 fossil_free(zBranch); 1513 } 1514 CX("\"dryRun\": %s", 1515 (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false"); 1516 if(blob_size(&manifest)>0){ 1517 CX(",\"manifest\": %!j", blob_str(&manifest)); 1518 } 1519 CX("}"); 1520 end_cleanup: 1521 db_end_transaction(0/*noting that dry-run mode will have already 1522 ** set this to rollback mode. */); 1523 fossil_free(zNewUuid); 1524 blob_reset(&err); 1525 blob_reset(&manifest); 1526 CheckinMiniInfo_cleanup(&cimi); 1527 } 1528 1529 /* 1530 ** WEBPAGE: fileedit 1531 ** 1532 ** Enables the online editing and committing of text files. Requires 1533 ** that the user have Write permissions and that a user with setup 1534 ** permissions has set the fileedit-glob setting to a list of glob 1535 ** patterns matching files which may be edited (e.g. "*.wiki,*.md"). 1536 ** Note that fileedit-glob, by design, is a local-only setting. 1537 ** It does not sync across repository clones, and must be explicitly 1538 ** set on any repositories where this page should be activated. 1539 ** 1540 ** Optional query parameters: 1541 ** 1542 ** filename=FILENAME Repo-relative path to the file. 1543 ** checkin=VERSION Checkin version, using any unambiguous 1544 ** symbolic version name. 1545 ** 1546 ** If passed a filename but no checkin then it will attempt to 1547 ** load that file from the most recent leaf checkin. 1548 ** 1549 ** Once the page is loaded, files may be selected from any open leaf 1550 ** version. The only way to edit files from non-leaf checkins is to 1551 ** pass both the filename and checkin as URL parameters to the page. 1552 ** Users with the proper permissions will be presented with "Edit" 1553 ** links in various file-specific contexts for files which match the 1554 ** fileedit-glob, regardless of whether they refer to leaf versions or 1555 ** not. 1556 */ 1557 void fileedit_page(void){ 1558 const char * zFileMime = 0; /* File mime type guess */ 1559 CheckinMiniInfo cimi; /* Checkin state */ 1560 int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */ 1561 Blob err = empty_blob; /* Error report */ 1562 const char *zAjax = P("name"); /* Name of AJAX route for 1563 sub-dispatching. */ 1564 1565 /* 1566 ** Internal-use URL parameters: 1567 ** 1568 ** name=string The name of a page-specific AJAX operation. 1569 ** 1570 ** Noting that fossil internally stores all URL path components 1571 ** after the first as the "name" value. Thus /fileedit?name=blah is 1572 ** equivalent to /fileedit/blah. The latter is the preferred 1573 ** form. This means, however, that no fileedit ajax routes may make 1574 ** use of the name parameter. 1575 ** 1576 ** Which additional parameters are used by each distinct ajax route 1577 ** is an internal implementation detail and may change with any 1578 ** given build of this code. An unknown "name" value triggers an 1579 ** error, as documented for ajax_route_error(). 1580 */ 1581 1582 /* Allow no access to this page without check-in privilege */ 1583 login_check_credentials(); 1584 if( !g.perm.Write ){ 1585 if(zAjax!=0){ 1586 ajax_route_error(403, "Write permissions required."); 1587 }else{ 1588 login_needed(g.anon.Write); 1589 } 1590 return; 1591 } 1592 /* No access to anything on this page if the fileedit-glob is empty */ 1593 if( fileedit_glob()==0 ){ 1594 if(zAjax!=0){ 1595 ajax_route_error(403, "Online editing is disabled for this " 1596 "repository."); 1597 return; 1598 } 1599 style_header("File Editor (disabled)"); 1600 CX("<h1>Online File Editing Is Disabled</h1>\n"); 1601 if( g.perm.Admin ){ 1602 CX("<p>To enable online editing, the " 1603 "<a href='%R/setup_settings'>" 1604 "<code>fileedit-glob</code> repository setting</a>\n" 1605 "must be set to a comma- and/or newine-delimited list of glob\n" 1606 "values matching files which may be edited online." 1607 "</p>\n"); 1608 }else{ 1609 CX("<p>Online editing is disabled for this repository.</p>\n"); 1610 } 1611 style_footer(); 1612 return; 1613 } 1614 1615 /* Dispatch AJAX methods based tail of the request URI. 1616 ** The AJAX parts do their own permissions/CSRF check and 1617 ** fail with a JSON-format response if needed. 1618 */ 1619 if( 0!=zAjax ){ 1620 /* preview mode is handled via /ajax/preview-text */ 1621 if(0==strcmp("content",zAjax)){ 1622 fileedit_ajax_content(); 1623 }else if(0==strcmp("filelist",zAjax)){ 1624 fileedit_ajax_filelist(); 1625 }else if(0==strcmp("diff",zAjax)){ 1626 fileedit_ajax_diff(); 1627 }else if(0==strcmp("commit",zAjax)){ 1628 fileedit_ajax_commit(); 1629 }else{ 1630 ajax_route_error(500, "Unhandled ajax route name."); 1631 } 1632 return; 1633 } 1634 1635 db_begin_transaction(); 1636 CheckinMiniInfo_init(&cimi); 1637 style_header("File Editor"); 1638 /* As of this point, don't use return or fossil_fatal(). Write any 1639 ** error in (&err) and goto end_footer instead so that we can be 1640 ** sure to emit the error message, do any cleanup, and end the 1641 ** transaction cleanly. 1642 */ 1643 { 1644 int isMissingArg = 0; 1645 if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){ 1646 assert(cimi.zFilename); 1647 zFileMime = mimetype_from_name(cimi.zFilename); 1648 }else if(isMissingArg!=0){ 1649 /* Squelch these startup warnings - they're non-fatal now but 1650 ** used to be fatal. */ 1651 blob_reset(&err); 1652 } 1653 } 1654 1655 /******************************************************************** 1656 ** All errors which "could" have happened up to this point are of a 1657 ** degree which keep us from rendering the rest of the page, and 1658 ** thus have already caused us to skipped to the end of the page to 1659 ** render the errors. Any up-coming errors, barring malloc failure 1660 ** or similar, are not "that" fatal. We can/should continue 1661 ** rendering the page, then output the error message at the end. 1662 ********************************************************************/ 1663 1664 /* The CSS for this page lives in a common file but much of it we 1665 ** don't want inadvertently being used by other pages. We don't 1666 ** have a common, page-specific container we can filter our CSS 1667 ** selectors, but we do have the BODY, which we can decorate with 1668 ** whatever CSS we wish... 1669 */ 1670 style_emit_script_tag(0,0); 1671 CX("document.body.classList.add('fileedit');\n"); 1672 style_emit_script_tag(1,0); 1673 1674 /* Status bar */ 1675 CX("<div id='fossil-status-bar' " 1676 "title='Status message area. Double-click to clear them.'>" 1677 "Status messages will go here.</div>\n" 1678 /* will be moved into the tab container via JS */); 1679 1680 CX("<div id='fileedit-edit-status'>" 1681 "<span class='name'>(no file loaded)</span>" 1682 "<span class='links'></span>" 1683 "</div>"); 1684 1685 /* Main tab container... */ 1686 CX("<div id='fileedit-tabs' class='tab-container'></div>"); 1687 1688 /* The .hidden class on the following tab elements is to help lessen 1689 the FOUC effect of the tabs before JS re-assembles them. */ 1690 1691 /***** File/version info tab *****/ 1692 { 1693 CX("<div id='fileedit-tab-fileselect' " 1694 "data-tab-parent='fileedit-tabs' " 1695 "data-tab-label='File Selection' " 1696 "class='hidden'" 1697 ">"); 1698 CX("<div id='fileedit-file-selector'></div>"); 1699 CX("</div>"/*#fileedit-tab-fileselect*/); 1700 } 1701 1702 /******* Content tab *******/ 1703 { 1704 CX("<div id='fileedit-tab-content' " 1705 "data-tab-parent='fileedit-tabs' " 1706 "data-tab-label='File Content' " 1707 "class='hidden'" 1708 ">"); 1709 CX("<div class='flex-container flex-row child-gap-small'>"); 1710 CX("<button class='fileedit-content-reload confirmer' " 1711 "title='Reload the file from the server, discarding " 1712 "any local edits. To help avoid accidental loss of " 1713 "edits, it requires confirmation (a second click) within " 1714 "a few seconds or it will not reload.'" 1715 ">Discard &amp; Reload</button>"); 1716 style_select_list_int("select-font-size", 1717 "editor_font_size", "Editor font size", 1718 NULL/*tooltip*/, 1719 100, 1720 "100%", 100, "125%", 125, 1721 "150%", 150, "175%", 175, 1722 "200%", 200, NULL); 1723 CX("</div>"); 1724 CX("<div class='flex-container flex-column stretch'>"); 1725 CX("<textarea name='content' id='fileedit-content-editor' " 1726 "class='fileedit' rows='25'>"); 1727 CX("</textarea>"); 1728 CX("</div>"/*textarea wrapper*/); 1729 CX("</div>"/*#tab-file-content*/); 1730 } 1731 1732 /****** Preview tab ******/ 1733 { 1734 CX("<div id='fileedit-tab-preview' " 1735 "data-tab-parent='fileedit-tabs' " 1736 "data-tab-label='Preview' " 1737 "class='hidden'" 1738 ">"); 1739 CX("<div class='fileedit-options flex-container flex-row'>"); 1740 CX("<button id='btn-preview-refresh' " 1741 "data-f-preview-from='fileContent' " 1742 /* ^^^ fossil.page[methodName]() OR text source elem ID, 1743 ** but we need a method in order to support clients swapping out 1744 ** the text editor with their own. */ 1745 "data-f-preview-via='_postPreview' " 1746 /* ^^^ fossil.page[methodName](content, callback) */ 1747 "data-f-preview-to='#fileedit-tab-preview-wrapper' " 1748 /* ^^^ dest elem ID */ 1749 ">Refresh</button>"); 1750 /* Toggle auto-update of preview when the Preview tab is selected. */ 1751 style_labeled_checkbox("cb-preview-autoupdate", 1752 NULL, 1753 "Auto-refresh?", 1754 "1", 1, 1755 "If on, the preview will automatically " 1756 "refresh when this tab is selected."); 1757 1758 /* Default preview rendering mode selection... */ 1759 previewRenderMode = zFileMime 1760 ? ajax_render_mode_for_mimetype(zFileMime) 1761 : AJAX_RENDER_GUESS; 1762 style_select_list_int("select-preview-mode", 1763 "preview_render_mode", 1764 "Preview Mode", 1765 "Preview mode format.", 1766 previewRenderMode, 1767 "Guess", AJAX_RENDER_GUESS, 1768 "Wiki/Markdown", AJAX_RENDER_WIKI, 1769 "HTML (iframe)", AJAX_RENDER_HTML_IFRAME, 1770 "HTML (inline)", AJAX_RENDER_HTML_INLINE, 1771 "Plain Text", AJAX_RENDER_PLAIN_TEXT, 1772 NULL); 1773 /* Allow selection of HTML preview iframe height */ 1774 style_select_list_int("select-preview-html-ems", 1775 "preview_html_ems", 1776 "HTML Preview IFrame Height (EMs)", 1777 "Height (in EMs) of the iframe used for " 1778 "HTML preview", 1779 40 /*default*/, 1780 "", 20, "", 40, 1781 "", 60, "", 80, 1782 "", 100, NULL); 1783 /* Selection of line numbers for text preview */ 1784 style_labeled_checkbox("cb-line-numbers", 1785 "preview_ln", 1786 "Add line numbers to plain-text previews?", 1787 "1", P("preview_ln")!=0, 1788 "If on, plain-text files (only) will get " 1789 "line numbers added to the preview."); 1790 CX("</div>"/*.fileedit-options*/); 1791 CX("<div id='fileedit-tab-preview-wrapper'></div>"); 1792 CX("</div>"/*#fileedit-tab-preview*/); 1793 } 1794 1795 /****** Diff tab ******/ 1796 { 1797 CX("<div id='fileedit-tab-diff' " 1798 "data-tab-parent='fileedit-tabs' " 1799 "data-tab-label='Diff' " 1800 "class='hidden'" 1801 ">"); 1802 1803 CX("<div class='fileedit-options flex-container " 1804 "flex-row child-gap-small' " 1805 "id='fileedit-tab-diff-buttons'>"); 1806 CX("<button class='sbs'>Side-by-side</button>" 1807 "<button class='unified'>Unified</button>"); 1808 if(0){ 1809 /* For the time being let's just ignore all whitespace 1810 ** changes, as files with Windows-style EOLs always show 1811 ** more diffs than we want then they're submitted to 1812 ** ?ajax=diff because JS normalizes them to Unix EOLs. 1813 ** We can revisit this decision later. */ 1814 style_select_list_int("diff-ws-policy", 1815 "diff_ws", "Whitespace", 1816 "Whitespace handling policy.", 1817 2, 1818 "Diff all whitespace", 0, 1819 "Ignore EOL whitespace", 1, 1820 "Ignore all whitespace", 2, 1821 NULL); 1822 } 1823 CX("</div>"); 1824 CX("<div id='fileedit-tab-diff-wrapper'>" 1825 "Diffs will be shown here." 1826 "</div>"); 1827 CX("</div>"/*#fileedit-tab-diff*/); 1828 } 1829 1830 /****** Commit ******/ 1831 CX("<div id='fileedit-tab-commit' " 1832 "data-tab-parent='fileedit-tabs' " 1833 "data-tab-label='Commit' " 1834 "class='hidden'" 1835 ">"); 1836 { 1837 /******* Commit flags/options *******/ 1838 CX("<div class='fileedit-options flex-container flex-row'>"); 1839 style_labeled_checkbox("cb-dry-run", 1840 "dry_run", "Dry-run?", "1", 1841 0, 1842 "In dry-run mode, the Commit button performs" 1843 "all work needed for committing changes but " 1844 "then rolls back the transaction, and thus " 1845 "does not really commit."); 1846 style_labeled_checkbox("cb-allow-fork", 1847 "allow_fork", "Allow fork?", "1", 1848 cimi.flags & CIMINI_ALLOW_FORK, 1849 "Allow committing to create a fork?"); 1850 style_labeled_checkbox("cb-allow-older", 1851 "allow_older", "Allow older?", "1", 1852 cimi.flags & CIMINI_ALLOW_OLDER, 1853 "Allow saving against a parent version " 1854 "which has a newer timestamp?"); 1855 style_labeled_checkbox("cb-exec-bit", 1856 "exec_bit", "Executable?", "1", 1857 PERM_EXE==cimi.filePerm, 1858 "Set the executable bit?"); 1859 style_labeled_checkbox("cb-allow-merge-conflict", 1860 "allow_merge_conflict", 1861 "Allow merge conflict markers?", "1", 1862 cimi.flags & CIMINI_ALLOW_MERGE_MARKER, 1863 "Allow saving even if the content contains " 1864 "what appear to be fossil merge conflict " 1865 "markers?"); 1866 style_labeled_checkbox("cb-prefer-delta", 1867 "prefer_delta", 1868 "Prefer delta manifest?", "1", 1869 db_get_boolean("forbid-delta-manifests",0) 1870 ? 0 1871 : (db_get_boolean("seen-delta-manifest",0) 1872 || cimi.flags & CIMINI_PREFER_DELTA), 1873 "Will create a delta manifest, instead of " 1874 "baseline, if conditions are favorable to " 1875 "do so. This option is only a suggestion."); 1876 style_labeled_checkbox("cb-include-manifest", 1877 "include_manifest", 1878 "Response manifest?", "1", 1879 0, 1880 "Include the manifest in the response? " 1881 "It's generally only useful for debug " 1882 "purposes."); 1883 style_select_list_int("select-eol-style", 1884 "eol", "EOL Style", 1885 "EOL conversion policy, noting that " 1886 "webpage-side processing may implicitly change " 1887 "the line endings of the input.", 1888 (cimi.flags & CIMINI_CONVERT_EOL_UNIX) 1889 ? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS 1890 ? 2 : 0), 1891 "Inherit", 0, 1892 "Unix", 1, 1893 "Windows", 2, 1894 NULL); 1895 1896 CX("</div>"/*checkboxes*/); 1897 } 1898 1899 { /******* Commit comment, button, and result manifest *******/ 1900 CX("<fieldset class='fileedit-options commit-message'>" 1901 "<legend>Message (required)</legend><div>\n"); 1902 /* We have two comment input fields, defaulting to single-line 1903 ** mode. JS code sets up the ability to toggle between single- 1904 ** and multi-line modes. */ 1905 CX("<input type='text' name='comment' " 1906 "id='fileedit-comment'></input>"); 1907 CX("<textarea name='commentBig' class='hidden' " 1908 "rows='5' id='fileedit-comment-big'></textarea>\n"); 1909 { /* comment options... */ 1910 CX("<div class='flex-container flex-column child-gap-small'>"); 1911 CX("<button id='comment-toggle' " 1912 "title='Toggle between single- and multi-line comment mode, " 1913 "noting that switching from multi- to single-line will cause " 1914 "newlines to get stripped.'" 1915 ">Toggle single-/multi-line</button> "); 1916 if(0){ 1917 /* Manifests support an N-card (comment mime type) but it has 1918 ** yet to be honored where comments are rendered, so we don't 1919 ** currently offer it as an option here: 1920 ** https://fossil-scm.org/forum/forumpost/662da045a1 1921 ** 1922 ** If/when it's ever implemented, simply enable this block and 1923 ** adjust the container's layout accordingly (as of this 1924 ** writing, that means changing the CSS class from 1925 ** 'flex-container flex-column' to 'flex-container flex-row'). 1926 */ 1927 style_select_list_str("comment-mimetype", "comment_mimetype", 1928 "Comment style:", 1929 "Specify how fossil will interpret the " 1930 "comment string.", 1931 NULL, 1932 "Fossil", "text/x-fossil-wiki", 1933 "Markdown", "text/x-markdown", 1934 "Plain text", "text/plain", 1935 NULL); 1936 CX("</div>\n"); 1937 } 1938 CX("<div class='fileedit-hint flex-container flex-row'>" 1939 "(Warning: switching from multi- to single-line mode will " 1940 "strip out all newlines!)</div>"); 1941 } 1942 CX("</div></fieldset>\n"/*commit comment options*/); 1943 CX("<div class='flex-container flex-column' " 1944 "id='fileedit-commit-button-wrapper'>" 1945 "<button id='fileedit-btn-commit'>Commit</button>" 1946 "</div>\n"); 1947 CX("<div id='fileedit-manifest'></div>\n" 1948 /* Manifest gets rendered here after a commit. */); 1949 } 1950 CX("</div>"/*#fileedit-tab-commit*/); 1951 1952 /****** Help/Tips ******/ 1953 CX("<div id='fileedit-tab-help' " 1954 "data-tab-parent='fileedit-tabs' " 1955 "data-tab-label='Help' " 1956 "class='hidden'" 1957 ">"); 1958 { 1959 CX("<h1>Help &amp; Tips</h1>"); 1960 CX("<ul>"); 1961 CX("<li><strong>Only files matching the <code>fileedit-glob</code> " 1962 "repository setting</strong> can be edited online. That setting " 1963 "must be a comma- or newline-delimited list of glob patterns " 1964 "for files which may be edited online.</li>"); 1965 CX("<li>Committing edits creates a new commit record with a single " 1966 "modified file.</li>"); 1967 CX("<li>\"Delta manifests\" (see the checkbox on the Commit tab) " 1968 "make for smaller commit records, especially in repositories " 1969 "with many files.</li>"); 1970 CX("<li>The file selector allows, for usability's sake, only files " 1971 "in leaf check-ins to be selected, but files may be edited via " 1972 "non-leaf check-ins by passing them as the <code>filename</code> " 1973 "and <code>checkin</code> URL arguments to this page.</li>"); 1974 CX("<li>The editor stores some number of local edits in one of " 1975 "<code>window.fileStorage</code> or " 1976 "<code>window.sessionStorage</code>, if able, but which storage " 1977 "is unspecified and may differ across environments. When " 1978 "committing or force-reloading a file, local edits to that " 1979 "file/check-in combination are discarded.</li>"); 1980 CX("</ul>"); 1981 } 1982 CX("</div>"/*#fileedit-tab-help*/); 1983 1984 builtin_request_js("sbsdiff.js"); 1985 style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer", 1986 "storage", 0); 1987 builtin_fulfill_js_requests(); 1988 /* 1989 ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is 1990 ** used for dynamically toggling certain UI components on and off. 1991 ** Must come after window.fossil has been intialized and before 1992 ** fossil.page.fileedit.js. Potential TODO: move this into the 1993 ** window.fossil bootstrapping so that we don't have to "fulfill" 1994 ** the JS multiple times. 1995 */ 1996 ajax_emit_js_preview_modes(1); 1997 builtin_request_js("fossil.page.fileedit.js"); 1998 builtin_fulfill_js_requests(); 1999 { 2000 /* Dynamically populate the editor, display any error in the err 2001 ** blob, and/or switch to tab #0, where the file selector 2002 ** lives. The extra C scopes here correspond to JS-level scopes, 2003 ** to improve grokability. */ 2004 style_emit_script_tag(0,0); 2005 CX("\n(function(){\n"); 2006 CX("try{\n"); 2007 { 2008 char * zFirstLeafUuid = 0; 2009 CX("fossil.config['fileedit-glob'] = "); 2010 glob_render_json_to_cgi(fileedit_glob()); 2011 CX(";\n"); 2012 if(blob_size(&err)>0){ 2013 CX("fossil.error(%!j);\n", blob_str(&err)); 2014 } 2015 /* Populate the page with the current leaves and, if available, 2016 the selected checkin's file list, to save 1 or 2 XHR requests 2017 at startup. That makes this page uncacheable, but compressed 2018 delivery of this page is currently less than 6k. */ 2019 CX("fossil.page.initialLeaves = "); 2020 fileedit_render_leaves_list(cimi.zParentUuid ? 0 : &zFirstLeafUuid); 2021 CX(";\n"); 2022 if(zFirstLeafUuid){ 2023 assert(!cimi.zParentUuid); 2024 cimi.zParentUuid = zFirstLeafUuid; 2025 zFirstLeafUuid = 0; 2026 } 2027 if(cimi.zParentUuid){ 2028 CX("fossil.page.initialFiles = "); 2029 fileedit_render_checkin_files(cimi.zParentUuid); 2030 CX(";\n"); 2031 } 2032 CX("fossil.onPageLoad(function(){\n"); 2033 { 2034 if(blob_size(&err)>0){ 2035 CX("fossil.error(%!j);\n", 2036 blob_str(&err)); 2037 CX("fossil.page.tabs.switchToTab(0);\n"); 2038 } 2039 if(cimi.zParentUuid && cimi.zFilename){ 2040 CX("fossil.page.loadFile(%!j,%!j);\n", 2041 cimi.zFilename, cimi.zParentUuid) 2042 /* Reminder we cannot embed the JSON-format 2043 content of the file here because if it contains 2044 a SCRIPT tag then it will break the whole page. */; 2045 } 2046 } 2047 CX("});\n")/*fossil.onPageLoad()*/; 2048 } 2049 CX("}catch(e){" 2050 "fossil.error(e); console.error('Exception:',e);" 2051 "}\n"); 2052 CX("})();")/*anonymous function*/; 2053 style_emit_script_tag(1,0); 2054 } 2055 blob_reset(&err); 2056 CheckinMiniInfo_cleanup(&cimi); 2057 db_end_transaction(0); 2058 style_footer(); 2059 }