Fossil

Check-in [a3bc6552ee]
Login

Check-in [a3bc6552ee]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Improvements to the Security-Audit page - mostly in providing additional information about the files in the CGI extension folder.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: a3bc6552eea4676d3bb55ed5b40ab608570e30b618d9edc3f2f6ce6ae4a77187
User & Date: drh 2019-08-04 20:23:46.904
Context
2019-08-04
20:26
Fix a harmless compiler warning. ... (check-in: 95a6a658d4 user: drh tags: trunk)
20:23
Improvements to the Security-Audit page - mostly in providing additional information about the files in the CGI extension folder. ... (check-in: a3bc6552ee user: drh tags: trunk)
2019-08-03
19:49
Fix the repolist function for CGI server invocations, apparently broken by check-in [ec56c69fe98d26d9] ... (check-in: 534b060007 user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/add.c.
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
    file_tree_name(g.argv[i], &fullName, 0, 1);
    blob_reset(&fullName);

    file_canonical_name(g.argv[i], &fullName, 0);
    zName = blob_str(&fullName);
    isDir = file_isdir(zName, RepoFILE);
    if( isDir==1 ){
      vfile_scan(&fullName, nRoot-1, scanFlags, pClean, pIgnore);
    }else if( isDir==0 ){
      fossil_warning("not found: %s", zName);
    }else{
      char *zTreeName = &zName[nRoot];
      if( !forceFlag && glob_match(pIgnore, zTreeName) ){
        Blob ans;
        char cReply;







|







326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
    file_tree_name(g.argv[i], &fullName, 0, 1);
    blob_reset(&fullName);

    file_canonical_name(g.argv[i], &fullName, 0);
    zName = blob_str(&fullName);
    isDir = file_isdir(zName, RepoFILE);
    if( isDir==1 ){
      vfile_scan(&fullName, nRoot-1, scanFlags, pClean, pIgnore, RepoFILE);
    }else if( isDir==0 ){
      fossil_warning("not found: %s", zName);
    }else{
      char *zTreeName = &zName[nRoot];
      if( !forceFlag && glob_match(pIgnore, zTreeName) ){
        Blob ans;
        char cReply;
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
  db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s)",
                filename_collation());
  n = strlen(g.zLocalRoot);
  blob_init(&path, g.zLocalRoot, n-1);
  /* now we read the complete file structure into a temp table */
  pClean = glob_create(zCleanFlag);
  pIgnore = glob_create(zIgnoreFlag);
  vfile_scan(&path, blob_size(&path), scanFlags, pClean, pIgnore);
  glob_free(pIgnore);
  glob_free(pClean);
  nAdd = add_files_in_sfile(vid);

  /* step 2: search for missing files */
  db_prepare(&q,
      "SELECT pathname, %Q || pathname, deleted FROM vfile"







|







675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
  db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s)",
                filename_collation());
  n = strlen(g.zLocalRoot);
  blob_init(&path, g.zLocalRoot, n-1);
  /* now we read the complete file structure into a temp table */
  pClean = glob_create(zCleanFlag);
  pIgnore = glob_create(zIgnoreFlag);
  vfile_scan(&path, blob_size(&path), scanFlags, pClean, pIgnore, RepoFILE);
  glob_free(pIgnore);
  glob_free(pClean);
  nAdd = add_files_in_sfile(vid);

  /* step 2: search for missing files */
  db_prepare(&q,
      "SELECT pathname, %Q || pathname, deleted FROM vfile"
Changes to src/checkin.c.
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  int nRoot;   /* length of g.zLocalRoot */

  db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s,"
                " mtime INTEGER, size INTEGER)", filename_collation());
  nRoot = (int)strlen(g.zLocalRoot);
  if( argc==0 ){
    blob_init(&name, g.zLocalRoot, nRoot - 1);
    vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0);
    blob_reset(&name);
  }else{
    for(i=0; i<argc; i++){
      file_canonical_name(argv[i], &name, 0);
      zName = blob_str(&name);
      isDir = file_isdir(zName, RepoFILE);
      if( isDir==1 ){
        vfile_scan(&name, nRoot-1, scanFlags, pIgnore, 0);
      }else if( isDir==0 ){
        fossil_warning("not found: %s", &zName[nRoot]);
      }else if( file_access(zName, R_OK) ){
        fossil_fatal("cannot open %s", &zName[nRoot]);
      }else{
        db_multi_exec(
           "INSERT OR IGNORE INTO sfile(pathname) VALUES(%Q)",







|







|







78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  int nRoot;   /* length of g.zLocalRoot */

  db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s,"
                " mtime INTEGER, size INTEGER)", filename_collation());
  nRoot = (int)strlen(g.zLocalRoot);
  if( argc==0 ){
    blob_init(&name, g.zLocalRoot, nRoot - 1);
    vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0, RepoFILE);
    blob_reset(&name);
  }else{
    for(i=0; i<argc; i++){
      file_canonical_name(argv[i], &name, 0);
      zName = blob_str(&name);
      isDir = file_isdir(zName, RepoFILE);
      if( isDir==1 ){
        vfile_scan(&name, nRoot-1, scanFlags, pIgnore, 0, RepoFILE);
      }else if( isDir==0 ){
        fossil_warning("not found: %s", &zName[nRoot]);
      }else if( file_access(zName, R_OK) ){
        fossil_fatal("cannot open %s", &zName[nRoot]);
      }else{
        db_multi_exec(
           "INSERT OR IGNORE INTO sfile(pathname) VALUES(%Q)",
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
  }
  if( emptyDirsFlag ){
    Glob *pEmptyDirs = glob_create(db_get("empty-dirs", 0));
    Stmt q;
    Blob root;
    blob_init(&root, g.zLocalRoot, nRoot - 1);
    vfile_dir_scan(&root, blob_size(&root), scanFlags, pIgnore,
                   pEmptyDirs);
    blob_reset(&root);
    db_prepare(&q,
        "SELECT %Q || x FROM dscan_temp"
        " WHERE x NOT IN (%s) AND y = 0"
        " ORDER BY 1 DESC",
        g.zLocalRoot, fossil_all_reserved_names(0)
    );







|







1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
  }
  if( emptyDirsFlag ){
    Glob *pEmptyDirs = glob_create(db_get("empty-dirs", 0));
    Stmt q;
    Blob root;
    blob_init(&root, g.zLocalRoot, nRoot - 1);
    vfile_dir_scan(&root, blob_size(&root), scanFlags, pIgnore,
                   pEmptyDirs, RepoFILE);
    blob_reset(&root);
    db_prepare(&q,
        "SELECT %Q || x FROM dscan_temp"
        " WHERE x NOT IN (%s) AND y = 0"
        " ORDER BY 1 DESC",
        g.zLocalRoot, fossil_all_reserved_names(0)
    );
Changes to src/extcgi.c.
72
73
74
75
76
77
78























79
80
81
82
83
84
85
   "SCRIPT_DIRECTORY",
   "SCRIPT_FILENAME",
   "SCRIPT_NAME",
   "SERVER_NAME",
   "SERVER_PORT",
   "SERVER_PROTOCOL",
};
























/*
** WEBPAGE: ext  raw-content
**
** Relay an HTTP request to secondary CGI after first checking the
** login credentials and setting auxiliary environment variables
** so that the secondary CGI can be aware of the credentials and







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
   "SCRIPT_DIRECTORY",
   "SCRIPT_FILENAME",
   "SCRIPT_NAME",
   "SERVER_NAME",
   "SERVER_PORT",
   "SERVER_PROTOCOL",
};

/*
** Check a pathname to determine if it is acceptable for use as
** extension CGI.  Some pathnames are excluded for security reasons.
** Return NULL on success or a static error string if there is
** a failure.
*/
const char *ext_pathname_ok(const char *zName){
  int i;
  const char *zFailReason = 0;
  for(i=0; zName[i]; i++){
    char c = zName[i];
    if( (c=='.' || c=='-') && (i==0 || zName[i-1]=='/') ){
      zFailReason = "path element begins with '.' or '-'";
      break;
    }
    if( !fossil_isalnum(c) && c!='_' && c!='-' && c!='.' && c!='/' ){
      zFailReason = "illegal character in path";
      break;
    }
  }
  return zFailReason;
}

/*
** WEBPAGE: ext  raw-content
**
** Relay an HTTP request to secondary CGI after first checking the
** login credentials and setting auxiliary environment variables
** so that the secondary CGI can be aware of the credentials and
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
    zFailReason = "extroot is a relative pathname";
    goto ext_not_found;
  }
  if( zName==0 ){
    zFailReason = "no path beyond /ext";
    goto ext_not_found;
  }
  for(i=0; zName[i]; i++){
    char c = zName[i];
    if( (c=='.' || c=='-') && (i==0 || zName[i-1]=='/') ){
      zFailReason = "path element begins with '.' or '-'";
      goto ext_not_found;
    }
    if( !fossil_isalnum(c) && c!='_' && c!='-' && c!='.' && c!='/' ){
      zFailReason = "illegal character in path";
      goto ext_not_found;
    }
  }
  if( file_isdir(g.zExtRoot,ExtFILE)!=1 ){
    zFailReason = "extroot is not a directory";
    goto ext_not_found;
  }
  zPath = mprintf("%s/%s", g.zExtRoot, zName);
  nRoot = (int)strlen(g.zExtRoot);
  if( file_isfile(zPath, ExtFILE) ){







<
<
<
|
|
<
<
|
<
<
<







152
153
154
155
156
157
158



159
160


161



162
163
164
165
166
167
168
    zFailReason = "extroot is a relative pathname";
    goto ext_not_found;
  }
  if( zName==0 ){
    zFailReason = "no path beyond /ext";
    goto ext_not_found;
  }



  zFailReason = ext_pathname_ok(zName);
  if( zFailReason ) goto ext_not_found;


  zFailReason = "???";



  if( file_isdir(g.zExtRoot,ExtFILE)!=1 ){
    zFailReason = "extroot is not a directory";
    goto ext_not_found;
  }
  zPath = mprintf("%s/%s", g.zExtRoot, zName);
  nRoot = (int)strlen(g.zExtRoot);
  if( file_isfile(zPath, ExtFILE) ){
295
296
297
298
299
300
301


































































    @ <p>Page not found: %h(zPathInfo)</p>
    if( g.perm.Debug ){
      @ <p>Reason for failure: %h(zFailReason)</p>
    }
  }
  return;
}









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
    @ <p>Page not found: %h(zPathInfo)</p>
    if( g.perm.Debug ){
      @ <p>Reason for failure: %h(zFailReason)</p>
    }
  }
  return;
}

/*
** Create a temporary SFILE table and fill it with one entry for each file
** in the extension document root directory (g.zExtRoot).  The SFILE table
** looks like this:
**
**    CREATE TEMP TABLE sfile(
**      pathname TEXT PRIMARY KEY,
**      isexe BOOLEAN
**    ) WITHOUT ROWID;
*/
void ext_files(void){
  Blob base;
  db_multi_exec(
     "CREATE TEMP TABLE sfile(\n"
     "  pathname TEXT PRIMARY KEY,\n"
     "  isexe BOOLEAN\n"
     ") WITHOUT ROWID;"
  );
  blob_init(&base, g.zExtRoot, -1);
  vfile_scan(&base, blob_size(&base),
             SCAN_ALL|SCAN_ISEXE,
             0, 0, ExtFILE);
  blob_zero(&base);
}

/*
** WEBPAGE: extfilelist
**
** List all files in the extension CGI document root and its subfolders.
*/
void ext_filelist_page(void){
  Stmt q;
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  ext_files();
  style_header("CGI Extension Filelist");
  @ <table border="0" cellspacing="0" cellpadding="3">
  @ <tbody>
  db_prepare(&q, "SELECT pathname, isexe FROM sfile"
                 " ORDER BY pathname");
  while( db_step(&q)==SQLITE_ROW ){
    const char *zName = db_column_text(&q,0);
    int isExe = db_column_int(&q,1);
    @ <tr>
    if( ext_pathname_ok(zName)!=0 ){
      @ <td><span style="opacity:0.5;">%h(zName)</span></td>
      @ <td>data file</td>
    }else{
      @ <td><a href="%R/ext/%h(zName)">%h(zName)</a></td>
      if( isExe ){
        @ <td>CGI</td>
      }else{
        @ <td>static content</td>
      }
    }
    @ </tr>
  }
  db_finalize(&q);
  @ </tbody>
  @ </table>
  style_footer();
}
Changes to src/main.c.
146
147
148
149
150
151
152

153
154
155
156
157
158
159
  sqlite3_int64 now;      /* Seconds since 1970 */
  int repositoryOpen;     /* True if the main repository database is open */
  unsigned iRepoDataVers;  /* Initial data version for repository database */
  char *zRepositoryOption; /* Most recent cached repository option value */
  char *zRepositoryName;  /* Name of the repository database file */
  char *zLocalDbName;     /* Name of the local database file */
  char *zOpenRevision;    /* Check-in version to use during database open */

  int localOpen;          /* True if the local database is open */
  char *zLocalRoot;       /* The directory holding the  local database */
  int minPrefix;          /* Number of digits needed for a distinct UUID */
  int eHashPolicy;        /* Current hash policy.  One of HPOLICY_* */
  int fSqlTrace;          /* True if --sqltrace flag is present */
  int fSqlStats;          /* True if --sqltrace or --sqlstats are present */
  int fSqlPrint;          /* True if --sqlprint flag is present */







>







146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
  sqlite3_int64 now;      /* Seconds since 1970 */
  int repositoryOpen;     /* True if the main repository database is open */
  unsigned iRepoDataVers;  /* Initial data version for repository database */
  char *zRepositoryOption; /* Most recent cached repository option value */
  char *zRepositoryName;  /* Name of the repository database file */
  char *zLocalDbName;     /* Name of the local database file */
  char *zOpenRevision;    /* Check-in version to use during database open */
  char *zCmdName;         /* Name of the Fossil command currently running */
  int localOpen;          /* True if the local database is open */
  char *zLocalRoot;       /* The directory holding the  local database */
  int minPrefix;          /* Number of digits needed for a distinct UUID */
  int eHashPolicy;        /* Current hash policy.  One of HPOLICY_* */
  int fSqlTrace;          /* True if --sqltrace flag is present */
  int fSqlStats;          /* True if --sqltrace or --sqlstats are present */
  int fSqlPrint;          /* True if --sqlprint flag is present */
784
785
786
787
788
789
790

791
792
793
794
795
796
797
      g.httpOut = stdout;
      g.fullHttpReply = !g.isHTTP;
      fossil_panic("file descriptor 2 is not open. (fd=%d, errno=%d)",
                   fd, x);
    }
  }
#endif

  rc = dispatch_name_search(zCmdName, CMDFLAG_COMMAND|CMDFLAG_PREFIX, &pCmd);
  if( rc==1 ){
#ifdef FOSSIL_ENABLE_TH1_HOOKS
    if( !g.isHTTP && !g.fNoThHook ){
      rc = Th_CommandHook(zCmdName, 0);
    }else{
      rc = TH_OK;







>







785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
      g.httpOut = stdout;
      g.fullHttpReply = !g.isHTTP;
      fossil_panic("file descriptor 2 is not open. (fd=%d, errno=%d)",
                   fd, x);
    }
  }
#endif
  g.zCmdName = zCmdName;
  rc = dispatch_name_search(zCmdName, CMDFLAG_COMMAND|CMDFLAG_PREFIX, &pCmd);
  if( rc==1 ){
#ifdef FOSSIL_ENABLE_TH1_HOOKS
    if( !g.isHTTP && !g.fNoThHook ){
      rc = Th_CommandHook(zCmdName, 0);
    }else{
      rc = TH_OK;
Changes to src/repolist.c.
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    /* The default case:  All repositories under the g.zRepositoryName
    ** directory.
    */
    blob_init(&base, g.zRepositoryName, -1);
    sqlite3_open(":memory:", &g.db);
    db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
    db_multi_exec("CREATE TABLE vfile(pathname);");
    vfile_scan(&base, blob_size(&base), 0, 0, 0);
    db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'");
    allRepo = 0;
  }
  n = db_int(0, "SELECT count(*) FROM sfile");
  if( n==0 ){
    sqlite3_close(g.db);
    return 0;







|







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    /* The default case:  All repositories under the g.zRepositoryName
    ** directory.
    */
    blob_init(&base, g.zRepositoryName, -1);
    sqlite3_open(":memory:", &g.db);
    db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
    db_multi_exec("CREATE TABLE vfile(pathname);");
    vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
    db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'");
    allRepo = 0;
  }
  n = db_int(0, "SELECT count(*) FROM sfile");
  if( n==0 ){
    sqlite3_close(g.db);
    return 0;
Changes to src/security_audit.c.
400
401
402
403
404
405
406
407
408
409
410
411
412
413

414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429





430
431

432
433
434
435
436
437
438
    }
  }
#endif

  if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
    @ <li><p>
    @ The server error log is disabled.
    @ To set up an error log:
    @ <ul>
    @ <li>If running from CGI, make an entry "errorlog: <i>FILENAME</i>"
    @ in the CGI script.
    @ <li>If running the "fossil server" or "fossil http" commands,
    @ add the "--errorlog <i>FILENAME</i>" command-line option.
    @ </ul>

  }else{
    FILE *pTest = fossil_fopen(g.zErrlog,"a");
    if( pTest==0 ){
      @ <li><p>
      @ <b>Error:</b>
      @ There is an error log at "%h(g.zErrlog)" but that file is not
      @ writable and so no logging will occur.
    }else{
      fclose(pTest);
      @ <li><p>
      @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" is
      @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
    }
  }

  if( g.zExtRoot ){





    @ <li><p> CGI Extensions are enabled with a document root
    @ of <b>%h(g.zExtRoot)</b>.

  }

  @ <li><p> User capability summary:
  capability_summary();

  if( alert_enabled() ){
    @ <li><p> Email alert configuration summary:







|
|
|
|
|
|
|
>
















>
>
>
>
>

|
>







400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
    }
  }
#endif

  if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
    @ <li><p>
    @ The server error log is disabled.
    @ To set up an error log,
    if( fossil_strcmp(g.zCmdName, "cgi")==0 ){
      @ make an entry like "errorlog: <i>FILENAME</i>" in the
      @ CGI script at %h(P("SCRIPT_FILENAME")).
    }else{
      @ add the "--errorlog <i>FILENAME</i>" option to the 
      @ "%h(g.argv[0]) %h(g.zCmdName)" command that launched this server.
    }
  }else{
    FILE *pTest = fossil_fopen(g.zErrlog,"a");
    if( pTest==0 ){
      @ <li><p>
      @ <b>Error:</b>
      @ There is an error log at "%h(g.zErrlog)" but that file is not
      @ writable and so no logging will occur.
    }else{
      fclose(pTest);
      @ <li><p>
      @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" is
      @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
    }
  }

  if( g.zExtRoot ){
    int nFile;
    int nCgi;
    ext_files();
    nFile = db_int(0, "SELECT count(*) FROM sfile");
    nCgi = nFile==0 ? 0 : db_int(0,"SELECT count(*) FROM sfile WHERE isexe");
    @ <li><p> CGI Extensions are enabled with a document root
    @ at <a href='%R/extfilelist'>%h(g.zExtRoot)</a> holding
    @ %d(nCgi) CGIs and %d(nFile-nCgi) static content and data files.
  }

  @ <li><p> User capability summary:
  capability_summary();

  if( alert_enabled() ){
    @ <li><p> Email alert configuration summary:
Changes to src/stat.c.
102
103
104
105
106
107
108



109

110
111
112
113
114
115
116
  @ </td></tr>
  nPend = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep");
  nDPend = db_int(0,"SELECT count(*) FROM pending_alert"
                    " WHERE NOT sentDigest");
  @ <tr><th>Pending&nbsp;Alerts:</th><td>
  @ %,d(nPend) normal, %,d(nDPend) digest
  @ </td></tr>



  @ <tr><th>Subscribers:</th><td>

  nSub = db_int(0, "SELECT count(*) FROM subscriber");
  nASub = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified"
                   " AND NOT sdonotcall AND length(ssub)>1");
  @ %,d(nASub) active, %,d(nSub) total
  @ </td></tr>
}








>
>
>
|
>







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
  @ </td></tr>
  nPend = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep");
  nDPend = db_int(0,"SELECT count(*) FROM pending_alert"
                    " WHERE NOT sentDigest");
  @ <tr><th>Pending&nbsp;Alerts:</th><td>
  @ %,d(nPend) normal, %,d(nDPend) digest
  @ </td></tr>
  if( g.perm.Admin ){
    @ <tr><th><a href="%R/subscribers">Subscribers:</a></th><td>
  }else{
    @ <tr><th>Subscribers:</th><td>
  }
  nSub = db_int(0, "SELECT count(*) FROM subscriber");
  nASub = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified"
                   " AND NOT sdonotcall AND length(ssub)>1");
  @ %,d(nASub) active, %,d(nSub) total
  @ </td></tr>
}

Changes to src/vfile.c.
428
429
430
431
432
433
434

435
436
437
438
439
440
441
442

443

444
445
446
447
448
449
450
451
452
453
454
455
456
457

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476

477
478










479
480
481
482

483
484

485
486

487
488
489
490
491
492
493
** Values for the scanFlags parameter to vfile_scan().
*/
#define SCAN_ALL    0x001    /* Includes files that begin with "." */
#define SCAN_TEMP   0x002    /* Only Fossil-generated files like *-baseline */
#define SCAN_NESTED 0x004    /* Scan for empty dirs in nested checkouts */
#define SCAN_MTIME  0x008    /* Populate mtime column */
#define SCAN_SIZE   0x010    /* Populate size column */

#endif /* INTERFACE */

/*
** Load into table SFILE the name of every ordinary file in
** the directory pPath.   Omit the first nPrefix characters of
** of pPath when inserting into the SFILE table.
**
** Subdirectories are scanned recursively.

** Omit files named in VFILE.

**
** Files whose names begin with "." are omitted unless the SCAN_ALL
** flag is set.
**
** Any files or directories that match the glob patterns pIgnore*
** are excluded from the scan.  Name matching occurs after the
** first nPrefix characters are elided from the filename.
*/
void vfile_scan(
  Blob *pPath,           /* Directory to be scanned */
  int nPrefix,           /* Number of bytes in directory name */
  unsigned scanFlags,    /* Zero or more SCAN_xxx flags */
  Glob *pIgnore1,        /* Do not add files that match this GLOB */
  Glob *pIgnore2         /* Omit files matching this GLOB too */

){
  DIR *d;
  int origSize;
  struct dirent *pEntry;
  int skipAll = 0;
  static Stmt ins;
  static int depth = 0;
  void *zNative;

  origSize = blob_size(pPath);
  if( pIgnore1 || pIgnore2 ){
    blob_appendf(pPath, "/");
    if( glob_match(pIgnore1, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
    if( glob_match(pIgnore2, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
    blob_resize(pPath, origSize);
  }
  if( skipAll ) return;

  if( depth==0 ){

    db_prepare(&ins,
      "INSERT OR IGNORE INTO sfile(pathname%s%s) SELECT :file%s%s"










      "  WHERE NOT EXISTS(SELECT 1 FROM vfile WHERE"
      " pathname=:file %s)",
      scanFlags & SCAN_MTIME ? ", mtime"  : "",
      scanFlags & SCAN_SIZE  ? ", size"   : "",

      scanFlags & SCAN_MTIME ? ", :mtime" : "",
      scanFlags & SCAN_SIZE  ? ", :size"  : "",

      filename_collation()
    );

  }
  depth++;

  zNative = fossil_utf8_to_path(blob_str(pPath), 1);
  d = opendir(zNative);
  if( d ){
    while( (pEntry=readdir(d))!=0 ){







>






<

>
|
>













|
>



















>
|
|
>
>
>
>
>
>
>
>
>
>
|
|
|
|
>
|
|
>
|
|
>







428
429
430
431
432
433
434
435
436
437
438
439
440
441

442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
** Values for the scanFlags parameter to vfile_scan().
*/
#define SCAN_ALL    0x001    /* Includes files that begin with "." */
#define SCAN_TEMP   0x002    /* Only Fossil-generated files like *-baseline */
#define SCAN_NESTED 0x004    /* Scan for empty dirs in nested checkouts */
#define SCAN_MTIME  0x008    /* Populate mtime column */
#define SCAN_SIZE   0x010    /* Populate size column */
#define SCAN_ISEXE  0x020    /* Populate isexe column */
#endif /* INTERFACE */

/*
** Load into table SFILE the name of every ordinary file in
** the directory pPath.   Omit the first nPrefix characters of
** of pPath when inserting into the SFILE table.

** Subdirectories are scanned recursively.
**
** Omit files named in VFILE if eFType==RepoFILE.  Include all files
** if eFType==ExtFILE.
**
** Files whose names begin with "." are omitted unless the SCAN_ALL
** flag is set.
**
** Any files or directories that match the glob patterns pIgnore*
** are excluded from the scan.  Name matching occurs after the
** first nPrefix characters are elided from the filename.
*/
void vfile_scan(
  Blob *pPath,           /* Directory to be scanned */
  int nPrefix,           /* Number of bytes in directory name */
  unsigned scanFlags,    /* Zero or more SCAN_xxx flags */
  Glob *pIgnore1,        /* Do not add files that match this GLOB */
  Glob *pIgnore2,        /* Omit files matching this GLOB too */
  int eFType             /* ExtFILE or RepoFILE */
){
  DIR *d;
  int origSize;
  struct dirent *pEntry;
  int skipAll = 0;
  static Stmt ins;
  static int depth = 0;
  void *zNative;

  origSize = blob_size(pPath);
  if( pIgnore1 || pIgnore2 ){
    blob_appendf(pPath, "/");
    if( glob_match(pIgnore1, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
    if( glob_match(pIgnore2, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
    blob_resize(pPath, origSize);
  }
  if( skipAll ) return;

  if( depth==0 ){
    if( eFType==ExtFILE ){
      db_prepare(&ins,
        "INSERT OR IGNORE INTO sfile(pathname%s%s%s) VALUES(:file%s%s%s)",
        scanFlags & SCAN_MTIME ? ",mtime"  : "",
        scanFlags & SCAN_SIZE  ? ",size"   : "",
        scanFlags & SCAN_ISEXE ? ",isexe"  : "",
        scanFlags & SCAN_MTIME ? ",:mtime" : "",
        scanFlags & SCAN_SIZE  ? ",:size"  : "",
        scanFlags & SCAN_ISEXE ? ",:isexe" : ""
      );
    }else{
      db_prepare(&ins,
        "INSERT OR IGNORE INTO sfile(pathname%s%s%s) SELECT :file%s%s%s"
        "  WHERE NOT EXISTS(SELECT 1 FROM vfile WHERE"
        " pathname=:file %s)",
        scanFlags & SCAN_MTIME ? ",mtime"  : "",
        scanFlags & SCAN_SIZE  ? ",size"   : "",
        scanFlags & SCAN_ISEXE ? ",isexe"  : "",
        scanFlags & SCAN_MTIME ? ",:mtime" : "",
        scanFlags & SCAN_SIZE  ? ",:size"  : "",
        scanFlags & SCAN_ISEXE ? ",:isexe" : "",
        filename_collation()
      );
    }
  }
  depth++;

  zNative = fossil_utf8_to_path(blob_str(pPath), 1);
  d = opendir(zNative);
  if( d ){
    while( (pEntry=readdir(d))!=0 ){
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528



529
530
531
532
533
534
535
      blob_appendf(pPath, "/%s", zUtf8);
      zPath = blob_str(pPath);
      if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
          glob_match(pIgnore2, &zPath[nPrefix+1]) ){
        /* do nothing */
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isdir(zPath, RepoFILE)==1) : (pEntry->d_type==DT_DIR) ){
#else
      }else if( file_isdir(zPath, RepoFILE)==1 ){
#endif
        if( !vfile_top_of_checkout(zPath) ){
          vfile_scan(pPath, nPrefix, scanFlags, pIgnore1, pIgnore2);
        }
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isfile_or_link(zPath)) : (pEntry->d_type==DT_REG) ){
#else
      }else if( file_isfile_or_link(zPath) ){
#endif
        if( (scanFlags & SCAN_TEMP)==0 || is_temporary_file(zUtf8) ){
          db_bind_text(&ins, ":file", &zPath[nPrefix+1]);
          if( scanFlags & SCAN_MTIME ){
            db_bind_int(&ins, ":mtime", file_mtime(zPath, RepoFILE));
          }
          if( scanFlags & SCAN_SIZE ){
            db_bind_int(&ins, ":size", file_size(zPath, RepoFILE));



          }
          db_step(&ins);
          db_reset(&ins);
        }
      }
      fossil_path_free(zUtf8);
      blob_resize(pPath, origSize);







|

|


|










|


|
>
>
>







519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
      blob_appendf(pPath, "/%s", zUtf8);
      zPath = blob_str(pPath);
      if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
          glob_match(pIgnore2, &zPath[nPrefix+1]) ){
        /* do nothing */
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isdir(zPath, eFType)==1) : (pEntry->d_type==DT_DIR) ){
#else
      }else if( file_isdir(zPath, eFType)==1 ){
#endif
        if( !vfile_top_of_checkout(zPath) ){
          vfile_scan(pPath, nPrefix, scanFlags, pIgnore1, pIgnore2, eFType);
        }
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isfile_or_link(zPath)) : (pEntry->d_type==DT_REG) ){
#else
      }else if( file_isfile_or_link(zPath) ){
#endif
        if( (scanFlags & SCAN_TEMP)==0 || is_temporary_file(zUtf8) ){
          db_bind_text(&ins, ":file", &zPath[nPrefix+1]);
          if( scanFlags & SCAN_MTIME ){
            db_bind_int(&ins, ":mtime", file_mtime(zPath, eFType));
          }
          if( scanFlags & SCAN_SIZE ){
            db_bind_int(&ins, ":size", file_size(zPath, eFType));
          }
          if( scanFlags & SCAN_ISEXE ){
            db_bind_int(&ins, ":isexe", file_isexe(zPath, eFType));
          }
          db_step(&ins);
          db_reset(&ins);
        }
      }
      fossil_path_free(zUtf8);
      blob_resize(pPath, origSize);
562
563
564
565
566
567
568
569

570
571
572
573
574
575
576
** Returns the total number of files found.
*/
int vfile_dir_scan(
  Blob *pPath,           /* Base directory to be scanned */
  int nPrefix,           /* Number of bytes in base directory name */
  unsigned scanFlags,    /* Zero or more SCAN_xxx flags */
  Glob *pIgnore1,        /* Do not add directories that match this GLOB */
  Glob *pIgnore2         /* Omit directories matching this GLOB too */

){
  int result = 0;
  DIR *d;
  int origSize;
  struct dirent *pEntry;
  int skipAll = 0;
  static Stmt ins;







|
>







582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
** Returns the total number of files found.
*/
int vfile_dir_scan(
  Blob *pPath,           /* Base directory to be scanned */
  int nPrefix,           /* Number of bytes in base directory name */
  unsigned scanFlags,    /* Zero or more SCAN_xxx flags */
  Glob *pIgnore1,        /* Do not add directories that match this GLOB */
  Glob *pIgnore2,        /* Omit directories matching this GLOB too */
  int eFType             /* ExtFILE or RepoFILE */
){
  int result = 0;
  DIR *d;
  int origSize;
  struct dirent *pEntry;
  int skipAll = 0;
  static Stmt ins;
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
      blob_appendf(pPath, "/%s", zUtf8);
      zPath = blob_str(pPath);
      if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
          glob_match(pIgnore2, &zPath[nPrefix+1]) ){
        /* do nothing */
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isdir(zPath, RepoFILE)==1) : (pEntry->d_type==DT_DIR) ){
#else
      }else if( file_isdir(zPath, RepoFILE)==1 ){
#endif
        if( (scanFlags & SCAN_NESTED) || !vfile_top_of_checkout(zPath) ){
          char *zSavePath = mprintf("%s", zPath);
          int count = vfile_dir_scan(pPath, nPrefix, scanFlags, pIgnore1,
                                     pIgnore2);
          db_bind_text(&ins, ":file", &zSavePath[nPrefix+1]);
          db_bind_int(&ins, ":count", count);
          db_step(&ins);
          db_reset(&ins);
          fossil_free(zSavePath);
          result += count; /* found X normal files? */
        }







|

|




|







643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
      blob_appendf(pPath, "/%s", zUtf8);
      zPath = blob_str(pPath);
      if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
          glob_match(pIgnore2, &zPath[nPrefix+1]) ){
        /* do nothing */
#ifdef _DIRENT_HAVE_D_TYPE
      }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
          ? (file_isdir(zPath, eFType)==1) : (pEntry->d_type==DT_DIR) ){
#else
      }else if( file_isdir(zPath, eFType)==1 ){
#endif
        if( (scanFlags & SCAN_NESTED) || !vfile_top_of_checkout(zPath) ){
          char *zSavePath = mprintf("%s", zPath);
          int count = vfile_dir_scan(pPath, nPrefix, scanFlags, pIgnore1,
                                     pIgnore2, eFType);
          db_bind_text(&ins, ":file", &zSavePath[nPrefix+1]);
          db_bind_int(&ins, ":count", count);
          db_step(&ins);
          db_reset(&ins);
          fossil_free(zSavePath);
          result += count; /* found X normal files? */
        }