diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/repolist.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/repolist.c')
| -rw-r--r-- | src/repolist.c | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/src/repolist.c b/src/repolist.c new file mode 100644 index 0000000..512c8f2 --- /dev/null +++ b/src/repolist.c @@ -0,0 +1,432 @@ +/* +** Copyright (c) 2018 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This module contains code to implement the repository list page when +** "fossil server" or "fossil ui" is serving a directory full of repositories. +*/ +#include "config.h" +#include "repolist.h" + +#if INTERFACE +/* +** Return value from the remote_repo_info() command. zRepoName is the +** input. All other fields are outputs. +*/ +struct RepoInfo { + char *zRepoName; /* Name of the repository file */ + int isValid; /* True if zRepoName is a valid Fossil repository */ + int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin + ** for the repository list. 2 means do use this + ** repository but do not display it in the list. */ + char *zProjName; /* Project Name. Memory from fossil_malloc() */ + char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ + char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ + double rMTime; /* Last update. Julian day number */ +}; +#endif + +/* +** Discover information about the repository given by +** pRepo->zRepoName. The discovered information is stored in other +** fields of the RepoInfo object. +*/ +static void remote_repo_info(RepoInfo *pRepo){ + sqlite3 *db; + sqlite3_stmt *pStmt; + int rc; + + pRepo->isRepolistSkin = 0; + pRepo->isValid = 0; + pRepo->zProjName = 0; + pRepo->zProjDesc = 0; + pRepo->zLoginGroup = 0; + pRepo->rMTime = 0.0; + + g.dbIgnoreErrors++; + rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); + if( rc ) goto finish_repo_list; + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" + " WHERE name='repolist-skin'", + -1, &pStmt, 0); + if( rc ) goto finish_repo_list; + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + pRepo->isRepolistSkin = sqlite3_column_int(pStmt,0); + } + sqlite3_finalize(pStmt); + if( rc ) goto finish_repo_list; + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" + " WHERE name='project-name'", + -1, &pStmt, 0); + if( rc ) goto finish_repo_list; + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); + } + sqlite3_finalize(pStmt); + if( rc ) goto finish_repo_list; + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" + " WHERE name='project-description'", + -1, &pStmt, 0); + if( rc ) goto finish_repo_list; + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); + } + sqlite3_finalize(pStmt); + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" + " WHERE name='login-group-name'", + -1, &pStmt, 0); + if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + pRepo->zLoginGroup = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); + } + sqlite3_finalize(pStmt); + rc = sqlite3_prepare_v2(db, "SELECT max(mtime) FROM event", -1, &pStmt, 0); + if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + pRepo->rMTime = sqlite3_column_double(pStmt,0); + } + pRepo->isValid = 1; + sqlite3_finalize(pStmt); +finish_repo_list: + g.dbIgnoreErrors--; + sqlite3_close(db); +} + +/* +** Generate a web-page that lists all repositories located under the +** g.zRepositoryName directory and return non-zero. +** +** For the special case when g.zRepositoryName is a non-chroot-jail "/", +** compose the list using the "repo:" entries in the global_config +** table of the configuration database. These entries comprise all +** of the repositories known to the "all" command. The special case +** processing is disallowed for chroot jails because g.zRepositoryName +** is always "/" inside a chroot jail and so it cannot be used as a flag +** to signal the special processing in that case. The special case +** processing is intended for the "fossil all ui" command which never +** runs in a chroot jail anyhow. +** +** Or, if no repositories can be located beneath g.zRepositoryName, +** close g.db and return 0. +*/ +int repo_list_page(void){ + Blob base; /* document root for all repositories */ + int n = 0; /* Number of repositories found */ + int allRepo; /* True if running "fossil ui all". + ** False if a directory scan of base for repos */ + Blob html; /* Html for the body of the repository list */ + char *zSkinRepo = 0; /* Name of the repository database used for skins */ + char *zSkinUrl = 0; /* URL for the skin database */ + const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ + int bShowDesc = 0; /* True to show the description column */ + int bShowLg = 0; /* True to show the login-group column */ + const char *zHome; /* Home page */ + + assert( g.db==0 ); + zShow = P("FOSSIL_REPOLIST_SHOW"); + if( zShow ){ + bShowDesc = strstr(zShow,"description")!=0; + bShowLg = strstr(zShow,"login-group")!=0; + } + blob_init(&html, 0, 0); + zHome = PD("home","home"); + if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ + /* For the special case of the "repository directory" being "/", + ** show all of the repositories named in the ~/.fossil database. + ** + ** On unix systems, then entries are of the form "repo:/home/..." + ** and on Windows systems they are like on unix, starting with a "/" + ** or they can begin with a drive letter: "repo:C:/Users/...". In either + ** case, we want returned path to omit any initial "/". + */ + db_open_config(1, 0); + db_multi_exec( + "CREATE TEMP VIEW sfile AS" + " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" + " WHERE name GLOB 'repo:*'" + ); + allRepo = 1; + }else{ + /* The default case: All repositories under the g.zRepositoryName + ** directory. + */ + Glob *pExclude; + blob_init(&base, g.zRepositoryName, -1); + db_close(0); + assert( g.db==0 ); + sqlite3_open(":memory:", &g.db); + db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); + db_multi_exec("CREATE TABLE vfile(pathname);"); + pExclude = glob_create("*/proc,proc"); + vfile_scan(&base, blob_size(&base), 0, pExclude, 0, ExtFILE); + glob_free(pExclude); + db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" +#if USE_SEE + " AND pathname NOT GLOB '*[^/].efossil'" +#endif + ); + allRepo = 0; + } + n = db_int(0, "SELECT count(*) FROM sfile"); + if( n==0 ){ + sqlite3_close(g.db); + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; + return 0; + }else{ + Stmt q; + double rNow; + char zType[16]; /* Column type letters for class "sortable" */ + int nType; + zType[0] = 't'; /* Repo name */ + zType[1] = 'x'; /* Space between repo-name and project-name */ + zType[2] = 't'; /* Project name */ + nType = 3; + if( bShowDesc ){ + zType[nType++] = 'x'; /* Space between name and description */ + zType[nType++] = 't'; /* Project description */ + } + zType[nType++] = 'x'; /* space before age */ + zType[nType++] = 'k'; /* Project age */ + if( bShowLg ){ + zType[nType++] = 'x'; /* space before login-group */ + zType[nType++] = 't'; /* Login Group */ + } + zType[nType] = 0; + blob_appendf(&html, + "<table border='0' class='sortable' data-init-sort='1'" + " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" + "<tr><th>Filename</th><th> </th>\n" + "<th%s><nobr>Project Name</nobr></th>\n", + zType, (bShowDesc ? " width='25%'" : "")); + if( bShowDesc ){ + blob_appendf(&html, + "<th> </th>\n" + "<th width='25%%'><nobr>Project Description</nobr></th>\n" + ); + } + blob_appendf(&html, + "<th> </th>" + "<th><nobr>Last Modified</nobr></th>\n" + ); + if( bShowLg ){ + blob_appendf(&html, + "<th> </th>" + "<th><nobr>Login Group</nobr></th></tr>\n" + ); + } + blob_appendf(&html,"</thead><tbody>\n"); + db_prepare(&q, "SELECT pathname" + " FROM sfile ORDER BY pathname COLLATE nocase;"); + rNow = db_double(0, "SELECT julianday('now')"); + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); + int nName = (int)strlen(zName); + int nSuffix = 7; /* ".fossil" */ + char *zUrl; + char *zAge; + char *zFull; + RepoInfo x; + sqlite3_int64 iAge; +#if USE_SEE + int bEncrypted = sqlite3_strglob("*.efossil", zName)==0; + if( bEncrypted ) nSuffix = 8; /* ".efossil" */ +#endif + if( nName<nSuffix ) continue; + zUrl = sqlite3_mprintf("%.*s", nName-nSuffix, zName); + if( zName[0]=='/' +#ifdef _WIN32 + || sqlite3_strglob("[a-zA-Z]:/*", zName)==0 +#endif + ){ + zFull = fossil_strdup(zName); + }else if ( allRepo ){ + zFull = mprintf("/%s", zName); + }else{ + zFull = mprintf("%s/%s", g.zRepositoryName, zName); + } + x.zRepoName = zFull; + remote_repo_info(&x); + if( x.isRepolistSkin ){ + if( zSkinRepo==0 ){ + zSkinRepo = fossil_strdup(x.zRepoName); + zSkinUrl = fossil_strdup(zUrl); + } + } + fossil_free(zFull); + if( !x.isValid +#if USE_SEE + && !bEncrypted +#endif + ){ + continue; + } + if( x.isRepolistSkin==2 && !allRepo ){ + /* Repositories with repolist-skin==2 are omitted from directory + ** scan lists, but included in "fossil all ui" lists */ + continue; + } + if( rNow <= x.rMTime ){ + x.rMTime = rNow; + }else if( x.rMTime<0.0 ){ + x.rMTime = rNow; + } + iAge = (sqlite3_int64)((rNow - x.rMTime)*86400); + zAge = human_readable_age(rNow - x.rMTime); + if( x.rMTime==0.0 ){ + /* This repository has no entry in the "event" table. + ** Its age will still be maximum, so data-sortkey will work. */ + zAge = mprintf("unknown"); + } + blob_appendf(&html, "<tr><td valign='top'><nobr>"); + if( !file_ends_with_repository_extension(zName,0) ){ + /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands + ** do not work for repositories whose names do not end in ".fossil". + ** So do not hyperlink those cases. */ + blob_appendf(&html,"%h",zName); + } else if( sqlite3_strglob("*/.*", zName)==0 ){ + /* Do not show hyperlinks for hidden repos */ + blob_appendf(&html, "%h (hidden)", zName); + } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ + blob_appendf(&html, + "<a href='%R/%T/%T' target='_blank'>/%h</a>\n", + zUrl, zHome, zName); + }else if( file_ends_with_repository_extension(zName,1) ){ + /* As described in + ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if + ** foo.fossil and foo/bar.fossil both exist and we create a + ** link to foo/bar/... then the URI dispatcher will instead + ** see that as a link to foo.fossil. In such cases, do not + ** emit a link to foo/bar.fossil. */ + char * zDirPart = file_dirname(zName); + if( db_exists("SELECT 1 FROM sfile " + "WHERE pathname=(%Q || '.fossil') COLLATE nocase" +#if USE_SEE + " OR pathname=(%Q || '.efossil') COLLATE nocase" +#endif + , zDirPart +#if USE_SEE + , zDirPart +#endif + ) ){ + blob_appendf(&html, + "<s>%h</s> (directory/repo name collision)\n", + zName); + }else{ + blob_appendf(&html, + "<a href='%R/%T/home' target='_blank'>%h</a>\n", + zUrl, zName); + } + fossil_free(zDirPart); + }else{ + blob_appendf(&html, + "<a href='%R/%T/home' target='_blank'>%h</a>\n", + zUrl, zName); + } + blob_appendf(&html,"</nobr></td>\n"); + if( x.zProjName ){ + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", + x.zProjName); + fossil_free(x.zProjName); + }else{ + blob_appendf(&html, "<td> </td><td></td>\n"); + } + if( !bShowDesc ){ + /* Do nothing */ + }else if( x.zProjDesc ){ + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", + x.zProjDesc); + fossil_free(x.zProjDesc); + }else{ + blob_appendf(&html, "<td> </td><td></td>\n"); + } + blob_appendf(&html, + "<td> </td><td data-sortkey='%08x' align='center' valign='top'>" + "<nobr>%h</nobr></td>\n", + (int)iAge, zAge); + fossil_free(zAge); + if( !bShowLg ){ + blob_appendf(&html, "</tr>\n"); + }else if( x.zLoginGroup ){ + blob_appendf(&html, "<td> </td><td valign='top'>" + "<nobr>%h</nobr></td></tr>\n", + x.zLoginGroup); + fossil_free(x.zLoginGroup); + }else{ + blob_appendf(&html, "<td> </td><td></td></tr>\n"); + } + sqlite3_free(zUrl); + } + db_finalize(&q); + blob_appendf(&html,"</tbody></table>\n"); + } + if( zSkinRepo ){ + char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); + g.zBaseURL = 0; + set_base_url(zNewBase); + db_open_repository(zSkinRepo); + fossil_free(zSkinRepo); + fossil_free(zSkinUrl); + } + if( g.repositoryOpen ){ + /* This case runs if remote_repo_info() found a repository + ** that has the "repolist_skin" property set to non-zero and left + ** that repository open in g.db. Use the skin of that repository + ** for display. */ + login_check_credentials(); + style_set_current_feature("repolist"); + style_header("Repository List"); + @ %s(blob_str(&html)) + style_table_sorter(); + style_finish_page(); + }else{ + const char *zTitle = PD("FOSSIL_REPOLIST_TITLE","Repository List"); + /* If no repositories were found that had the "repolist_skin" + ** property set, then use a default skin */ + @ <html> + @ <head> + @ <base href="%s(g.zBaseURL)/"> + @ <meta name="viewport" content="width=device-width, initial-scale=1.0"> + @ <title>%h(zTitle)</title> + @ </head> + @ <body> + @ <h1 align="center">%h(zTitle)</h1> + @ %s(blob_str(&html)) + @ <script>%s(builtin_text("sorttable.js"))</script> + @ </body> + @ </html> + } + blob_reset(&html); + cgi_reply(); + return n; +} + +/* +** COMMAND: test-list-page +** +** Usage: %fossil test-list-page DIRECTORY +** +** Show all repositories underneath DIRECTORY. Or if DIRECTORY is "/" +** show all repositories in the ~/.fossil file. +*/ +void test_list_page(void){ + if( g.argc<3 ){ + g.zRepositoryName = "/"; + }else{ + g.zRepositoryName = g.argv[2]; + } + g.httpOut = stdout; + repo_list_page(); +} |
