|
1
|
/* |
|
2
|
** Copyright (c) 2018 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
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This module contains code to implement the repository list page when |
|
19
|
** "fossil server" or "fossil ui" is serving a directory full of repositories. |
|
20
|
*/ |
|
21
|
#include "config.h" |
|
22
|
#include "repolist.h" |
|
23
|
|
|
24
|
#if INTERFACE |
|
25
|
/* |
|
26
|
** Return value from the remote_repo_info() command. zRepoName is the |
|
27
|
** input. All other fields are outputs. |
|
28
|
*/ |
|
29
|
struct RepoInfo { |
|
30
|
char *zRepoName; /* Name of the repository file */ |
|
31
|
int isValid; /* True if zRepoName is a valid Fossil repository */ |
|
32
|
int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
|
33
|
** for the repository list. 2 means do use this |
|
34
|
** repository but do not display it in the list. */ |
|
35
|
char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
|
36
|
char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ |
|
37
|
char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
|
38
|
double rMTime; /* Last update. Julian day number */ |
|
39
|
}; |
|
40
|
#endif |
|
41
|
|
|
42
|
/* |
|
43
|
** Discover information about the repository given by |
|
44
|
** pRepo->zRepoName. The discovered information is stored in other |
|
45
|
** fields of the RepoInfo object. |
|
46
|
*/ |
|
47
|
static void remote_repo_info(RepoInfo *pRepo){ |
|
48
|
sqlite3 *db; |
|
49
|
sqlite3_stmt *pStmt; |
|
50
|
int rc; |
|
51
|
|
|
52
|
pRepo->isRepolistSkin = 0; |
|
53
|
pRepo->isValid = 0; |
|
54
|
pRepo->zProjName = 0; |
|
55
|
pRepo->zProjDesc = 0; |
|
56
|
pRepo->zLoginGroup = 0; |
|
57
|
pRepo->rMTime = 0.0; |
|
58
|
|
|
59
|
g.dbIgnoreErrors++; |
|
60
|
rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
|
61
|
if( rc ) goto finish_repo_list; |
|
62
|
rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
|
63
|
" WHERE name='repolist-skin'", |
|
64
|
-1, &pStmt, 0); |
|
65
|
if( rc ) goto finish_repo_list; |
|
66
|
if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
|
67
|
pRepo->isRepolistSkin = sqlite3_column_int(pStmt,0); |
|
68
|
} |
|
69
|
sqlite3_finalize(pStmt); |
|
70
|
if( rc ) goto finish_repo_list; |
|
71
|
rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
|
72
|
" WHERE name='project-name'", |
|
73
|
-1, &pStmt, 0); |
|
74
|
if( rc ) goto finish_repo_list; |
|
75
|
if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
|
76
|
pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
|
77
|
} |
|
78
|
sqlite3_finalize(pStmt); |
|
79
|
if( rc ) goto finish_repo_list; |
|
80
|
rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
|
81
|
" WHERE name='project-description'", |
|
82
|
-1, &pStmt, 0); |
|
83
|
if( rc ) goto finish_repo_list; |
|
84
|
if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
|
85
|
pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
|
86
|
} |
|
87
|
sqlite3_finalize(pStmt); |
|
88
|
rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
|
89
|
" WHERE name='login-group-name'", |
|
90
|
-1, &pStmt, 0); |
|
91
|
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
|
92
|
pRepo->zLoginGroup = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
|
93
|
} |
|
94
|
sqlite3_finalize(pStmt); |
|
95
|
rc = sqlite3_prepare_v2(db, "SELECT max(mtime) FROM event", -1, &pStmt, 0); |
|
96
|
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
|
97
|
pRepo->rMTime = sqlite3_column_double(pStmt,0); |
|
98
|
} |
|
99
|
pRepo->isValid = 1; |
|
100
|
sqlite3_finalize(pStmt); |
|
101
|
finish_repo_list: |
|
102
|
g.dbIgnoreErrors--; |
|
103
|
sqlite3_close(db); |
|
104
|
} |
|
105
|
|
|
106
|
/* |
|
107
|
** Generate a web-page that lists all repositories located under the |
|
108
|
** g.zRepositoryName directory and return non-zero. |
|
109
|
** |
|
110
|
** For the special case when g.zRepositoryName is a non-chroot-jail "/", |
|
111
|
** compose the list using the "repo:" entries in the global_config |
|
112
|
** table of the configuration database. These entries comprise all |
|
113
|
** of the repositories known to the "all" command. The special case |
|
114
|
** processing is disallowed for chroot jails because g.zRepositoryName |
|
115
|
** is always "/" inside a chroot jail and so it cannot be used as a flag |
|
116
|
** to signal the special processing in that case. The special case |
|
117
|
** processing is intended for the "fossil all ui" command which never |
|
118
|
** runs in a chroot jail anyhow. |
|
119
|
** |
|
120
|
** Or, if no repositories can be located beneath g.zRepositoryName, |
|
121
|
** close g.db and return 0. |
|
122
|
*/ |
|
123
|
int repo_list_page(void){ |
|
124
|
Blob base; /* document root for all repositories */ |
|
125
|
int n = 0; /* Number of repositories found */ |
|
126
|
int allRepo; /* True if running "fossil ui all". |
|
127
|
** False if a directory scan of base for repos */ |
|
128
|
Blob html; /* Html for the body of the repository list */ |
|
129
|
char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
|
130
|
char *zSkinUrl = 0; /* URL for the skin database */ |
|
131
|
const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ |
|
132
|
int bShowDesc = 0; /* True to show the description column */ |
|
133
|
int bShowLg = 0; /* True to show the login-group column */ |
|
134
|
|
|
135
|
assert( g.db==0 ); |
|
136
|
zShow = P("FOSSIL_REPOLIST_SHOW"); |
|
137
|
if( zShow ){ |
|
138
|
bShowDesc = strstr(zShow,"description")!=0; |
|
139
|
bShowLg = strstr(zShow,"login-group")!=0; |
|
140
|
} |
|
141
|
blob_init(&html, 0, 0); |
|
142
|
if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
|
143
|
/* For the special case of the "repository directory" being "/", |
|
144
|
** show all of the repositories named in the ~/.fossil database. |
|
145
|
** |
|
146
|
** On unix systems, then entries are of the form "repo:/home/..." |
|
147
|
** and on Windows systems they are like on unix, starting with a "/" |
|
148
|
** or they can begin with a drive letter: "repo:C:/Users/...". In either |
|
149
|
** case, we want returned path to omit any initial "/". |
|
150
|
*/ |
|
151
|
db_open_config(1, 0); |
|
152
|
db_multi_exec( |
|
153
|
"CREATE TEMP VIEW sfile AS" |
|
154
|
" SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
|
155
|
" WHERE name GLOB 'repo:*'" |
|
156
|
); |
|
157
|
allRepo = 1; |
|
158
|
}else{ |
|
159
|
/* The default case: All repositories under the g.zRepositoryName |
|
160
|
** directory. |
|
161
|
*/ |
|
162
|
Glob *pExclude; |
|
163
|
blob_init(&base, g.zRepositoryName, -1); |
|
164
|
db_close(0); |
|
165
|
assert( g.db==0 ); |
|
166
|
sqlite3_open(":memory:", &g.db); |
|
167
|
db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
|
168
|
db_multi_exec("CREATE TABLE vfile(pathname);"); |
|
169
|
pExclude = glob_create("*/proc,proc"); |
|
170
|
vfile_scan(&base, blob_size(&base), 0, pExclude, 0, ExtFILE); |
|
171
|
glob_free(pExclude); |
|
172
|
db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
|
173
|
#if USE_SEE |
|
174
|
" AND pathname NOT GLOB '*[^/].efossil'" |
|
175
|
#endif |
|
176
|
); |
|
177
|
allRepo = 0; |
|
178
|
} |
|
179
|
n = db_int(0, "SELECT count(*) FROM sfile"); |
|
180
|
if( n==0 ){ |
|
181
|
sqlite3_close(g.db); |
|
182
|
g.db = 0; |
|
183
|
g.repositoryOpen = 0; |
|
184
|
g.localOpen = 0; |
|
185
|
return 0; |
|
186
|
}else{ |
|
187
|
Stmt q; |
|
188
|
double rNow; |
|
189
|
char zType[16]; /* Column type letters for class "sortable" */ |
|
190
|
int nType; |
|
191
|
zType[0] = 't'; /* Repo name */ |
|
192
|
zType[1] = 'x'; /* Space between repo-name and project-name */ |
|
193
|
zType[2] = 't'; /* Project name */ |
|
194
|
nType = 3; |
|
195
|
if( bShowDesc ){ |
|
196
|
zType[nType++] = 'x'; /* Space between name and description */ |
|
197
|
zType[nType++] = 't'; /* Project description */ |
|
198
|
} |
|
199
|
zType[nType++] = 'x'; /* space before age */ |
|
200
|
zType[nType++] = 'k'; /* Project age */ |
|
201
|
if( bShowLg ){ |
|
202
|
zType[nType++] = 'x'; /* space before login-group */ |
|
203
|
zType[nType++] = 't'; /* Login Group */ |
|
204
|
} |
|
205
|
zType[nType] = 0; |
|
206
|
blob_appendf(&html, |
|
207
|
"<table border='0' class='sortable' data-init-sort='1'" |
|
208
|
" data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" |
|
209
|
"<tr><th>Filename</th><th> </th>\n" |
|
210
|
"<th%s><nobr>Project Name</nobr></th>\n", |
|
211
|
zType, (bShowDesc ? " width='25%'" : "")); |
|
212
|
if( bShowDesc ){ |
|
213
|
blob_appendf(&html, |
|
214
|
"<th> </th>\n" |
|
215
|
"<th width='25%%'><nobr>Project Description</nobr></th>\n" |
|
216
|
); |
|
217
|
} |
|
218
|
blob_appendf(&html, |
|
219
|
"<th> </th>" |
|
220
|
"<th><nobr>Last Modified</nobr></th>\n" |
|
221
|
); |
|
222
|
if( bShowLg ){ |
|
223
|
blob_appendf(&html, |
|
224
|
"<th> </th>" |
|
225
|
"<th><nobr>Login Group</nobr></th></tr>\n" |
|
226
|
); |
|
227
|
} |
|
228
|
blob_appendf(&html,"</thead><tbody>\n"); |
|
229
|
db_prepare(&q, "SELECT pathname" |
|
230
|
" FROM sfile ORDER BY pathname COLLATE nocase;"); |
|
231
|
rNow = db_double(0, "SELECT julianday('now')"); |
|
232
|
while( db_step(&q)==SQLITE_ROW ){ |
|
233
|
const char *zName = db_column_text(&q, 0); |
|
234
|
int nName = (int)strlen(zName); |
|
235
|
int nSuffix = 7; /* ".fossil" */ |
|
236
|
char *zUrl; |
|
237
|
char *zAge; |
|
238
|
char *zFull; |
|
239
|
RepoInfo x; |
|
240
|
sqlite3_int64 iAge; |
|
241
|
#if USE_SEE |
|
242
|
int bEncrypted = sqlite3_strglob("*.efossil", zName)==0; |
|
243
|
if( bEncrypted ) nSuffix = 8; /* ".efossil" */ |
|
244
|
#endif |
|
245
|
if( nName<nSuffix ) continue; |
|
246
|
zUrl = sqlite3_mprintf("%.*s", nName-nSuffix, zName); |
|
247
|
if( zName[0]=='/' |
|
248
|
#ifdef _WIN32 |
|
249
|
|| sqlite3_strglob("[a-zA-Z]:/*", zName)==0 |
|
250
|
#endif |
|
251
|
){ |
|
252
|
zFull = fossil_strdup(zName); |
|
253
|
}else if ( allRepo ){ |
|
254
|
zFull = mprintf("/%s", zName); |
|
255
|
}else{ |
|
256
|
zFull = mprintf("%s/%s", g.zRepositoryName, zName); |
|
257
|
} |
|
258
|
x.zRepoName = zFull; |
|
259
|
remote_repo_info(&x); |
|
260
|
if( x.isRepolistSkin ){ |
|
261
|
if( zSkinRepo==0 ){ |
|
262
|
zSkinRepo = fossil_strdup(x.zRepoName); |
|
263
|
zSkinUrl = fossil_strdup(zUrl); |
|
264
|
} |
|
265
|
} |
|
266
|
fossil_free(zFull); |
|
267
|
if( !x.isValid |
|
268
|
#if USE_SEE |
|
269
|
&& !bEncrypted |
|
270
|
#endif |
|
271
|
){ |
|
272
|
continue; |
|
273
|
} |
|
274
|
if( x.isRepolistSkin==2 && !allRepo ){ |
|
275
|
/* Repositories with repolist-skin==2 are omitted from directory |
|
276
|
** scan lists, but included in "fossil all ui" lists */ |
|
277
|
continue; |
|
278
|
} |
|
279
|
if( rNow <= x.rMTime ){ |
|
280
|
x.rMTime = rNow; |
|
281
|
}else if( x.rMTime<0.0 ){ |
|
282
|
x.rMTime = rNow; |
|
283
|
} |
|
284
|
iAge = (sqlite3_int64)((rNow - x.rMTime)*86400); |
|
285
|
zAge = human_readable_age(rNow - x.rMTime); |
|
286
|
if( x.rMTime==0.0 ){ |
|
287
|
/* This repository has no entry in the "event" table. |
|
288
|
** Its age will still be maximum, so data-sortkey will work. */ |
|
289
|
zAge = mprintf("unknown"); |
|
290
|
} |
|
291
|
blob_appendf(&html, "<tr><td valign='top'><nobr>"); |
|
292
|
if( !file_ends_with_repository_extension(zName,0) ){ |
|
293
|
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
|
294
|
** do not work for repositories whose names do not end in ".fossil". |
|
295
|
** So do not hyperlink those cases. */ |
|
296
|
blob_appendf(&html,"%h",zName); |
|
297
|
} else if( sqlite3_strglob("*/.*", zName)==0 ){ |
|
298
|
/* Do not show hyperlinks for hidden repos */ |
|
299
|
blob_appendf(&html, "%h (hidden)", zName); |
|
300
|
} else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
|
301
|
blob_appendf(&html, |
|
302
|
"<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
|
303
|
zUrl, zName); |
|
304
|
}else if( file_ends_with_repository_extension(zName,1) ){ |
|
305
|
/* As described in |
|
306
|
** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
|
307
|
** foo.fossil and foo/bar.fossil both exist and we create a |
|
308
|
** link to foo/bar/... then the URI dispatcher will instead |
|
309
|
** see that as a link to foo.fossil. In such cases, do not |
|
310
|
** emit a link to foo/bar.fossil. */ |
|
311
|
char * zDirPart = file_dirname(zName); |
|
312
|
if( db_exists("SELECT 1 FROM sfile " |
|
313
|
"WHERE pathname=(%Q || '.fossil') COLLATE nocase" |
|
314
|
#if USE_SEE |
|
315
|
" OR pathname=(%Q || '.efossil') COLLATE nocase" |
|
316
|
#endif |
|
317
|
, zDirPart |
|
318
|
#if USE_SEE |
|
319
|
, zDirPart |
|
320
|
#endif |
|
321
|
) ){ |
|
322
|
blob_appendf(&html, |
|
323
|
"<s>%h</s> (directory/repo name collision)\n", |
|
324
|
zName); |
|
325
|
}else{ |
|
326
|
blob_appendf(&html, |
|
327
|
"<a href='%R/%T/home' target='_blank'>%h</a>\n", |
|
328
|
zUrl, zName); |
|
329
|
} |
|
330
|
fossil_free(zDirPart); |
|
331
|
}else{ |
|
332
|
blob_appendf(&html, |
|
333
|
"<a href='%R/%T/home' target='_blank'>%h</a>\n", |
|
334
|
zUrl, zName); |
|
335
|
} |
|
336
|
blob_appendf(&html,"</nobr></td>\n"); |
|
337
|
if( x.zProjName ){ |
|
338
|
blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
|
339
|
x.zProjName); |
|
340
|
fossil_free(x.zProjName); |
|
341
|
}else{ |
|
342
|
blob_appendf(&html, "<td> </td><td></td>\n"); |
|
343
|
} |
|
344
|
if( !bShowDesc ){ |
|
345
|
/* Do nothing */ |
|
346
|
}else if( x.zProjDesc ){ |
|
347
|
blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
|
348
|
x.zProjDesc); |
|
349
|
fossil_free(x.zProjDesc); |
|
350
|
}else{ |
|
351
|
blob_appendf(&html, "<td> </td><td></td>\n"); |
|
352
|
} |
|
353
|
blob_appendf(&html, |
|
354
|
"<td> </td><td data-sortkey='%08x' align='center' valign='top'>" |
|
355
|
"<nobr>%h</nobr></td>\n", |
|
356
|
(int)iAge, zAge); |
|
357
|
fossil_free(zAge); |
|
358
|
if( !bShowLg ){ |
|
359
|
blob_appendf(&html, "</tr>\n"); |
|
360
|
}else if( x.zLoginGroup ){ |
|
361
|
blob_appendf(&html, "<td> </td><td valign='top'>" |
|
362
|
"<nobr>%h</nobr></td></tr>\n", |
|
363
|
x.zLoginGroup); |
|
364
|
fossil_free(x.zLoginGroup); |
|
365
|
}else{ |
|
366
|
blob_appendf(&html, "<td> </td><td></td></tr>\n"); |
|
367
|
} |
|
368
|
sqlite3_free(zUrl); |
|
369
|
} |
|
370
|
db_finalize(&q); |
|
371
|
blob_appendf(&html,"</tbody></table>\n"); |
|
372
|
} |
|
373
|
if( zSkinRepo ){ |
|
374
|
char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
|
375
|
g.zBaseURL = 0; |
|
376
|
set_base_url(zNewBase); |
|
377
|
db_open_repository(zSkinRepo); |
|
378
|
fossil_free(zSkinRepo); |
|
379
|
fossil_free(zSkinUrl); |
|
380
|
} |
|
381
|
if( g.repositoryOpen ){ |
|
382
|
/* This case runs if remote_repo_info() found a repository |
|
383
|
** that has the "repolist_skin" property set to non-zero and left |
|
384
|
** that repository open in g.db. Use the skin of that repository |
|
385
|
** for display. */ |
|
386
|
login_check_credentials(); |
|
387
|
style_set_current_feature("repolist"); |
|
388
|
style_header("Repository List"); |
|
389
|
@ %s(blob_str(&html)) |
|
390
|
style_table_sorter(); |
|
391
|
style_finish_page(); |
|
392
|
}else{ |
|
393
|
const char *zTitle = PD("FOSSIL_REPOLIST_TITLE","Repository List"); |
|
394
|
/* If no repositories were found that had the "repolist_skin" |
|
395
|
** property set, then use a default skin */ |
|
396
|
@ <html> |
|
397
|
@ <head> |
|
398
|
@ <base href="%s(g.zBaseURL)/"> |
|
399
|
@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
400
|
@ <title>%h(zTitle)</title> |
|
401
|
@ </head> |
|
402
|
@ <body> |
|
403
|
@ <h1 align="center">%h(zTitle)</h1> |
|
404
|
@ %s(blob_str(&html)) |
|
405
|
@ <script>%s(builtin_text("sorttable.js"))</script> |
|
406
|
@ </body> |
|
407
|
@ </html> |
|
408
|
} |
|
409
|
blob_reset(&html); |
|
410
|
cgi_reply(); |
|
411
|
return n; |
|
412
|
} |
|
413
|
|
|
414
|
/* |
|
415
|
** COMMAND: test-list-page |
|
416
|
** |
|
417
|
** Usage: %fossil test-list-page DIRECTORY |
|
418
|
** |
|
419
|
** Show all repositories underneath DIRECTORY. Or if DIRECTORY is "/" |
|
420
|
** show all repositories in the ~/.fossil file. |
|
421
|
*/ |
|
422
|
void test_list_page(void){ |
|
423
|
if( g.argc<3 ){ |
|
424
|
g.zRepositoryName = "/"; |
|
425
|
}else{ |
|
426
|
g.zRepositoryName = g.argv[2]; |
|
427
|
} |
|
428
|
g.httpOut = stdout; |
|
429
|
repo_list_page(); |
|
430
|
} |
|
431
|
|