/* ** Copyright (c) 2002 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public ** License as published by the Free Software Foundation; either ** version 2 of the License, or (at your option) any later version. ** ** 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. See the GNU ** General Public License for more details. ** ** You should have received a copy of the GNU General Public ** License along with this library; if not, write to the ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, ** Boston, MA 02111-1307, USA. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** Implementation of the Setup page */ #include #include "config.h" #include "setup.h" /* ** Output a single entry for a menu generated using an HTML table. ** If zLink is not NULL or an empty string, then it is the page that ** the menu entry will hyperlink to. If zLink is NULL or "", then ** the menu entry has no hyperlink - it is disabled. */ static void menu_entry( const char *zTitle, const char *zLink, const char *zDesc ){ @ if( zLink && zLink[0] ){ @ %s(zTitle) }else{ @ %s(zTitle) } @ @ %s(zDesc) @ @ } /* ** WEBPAGE: /setup */ void setup_page(void){ /* The user must be at least the administrator in order to see ** this screen. */ login_check_credentials(); if( !g.okAdmin ){ login_needed(); return; } common_standard_menu("setup", 0); common_add_help_item("CvstracAdmin"); common_header("Setup Menu"); @ if( g.okSetup ){ menu_entry(mprintf("%s Repository",g.scm.zName), "setup_repository", "Identify the repository to which this server is linked."); if( g.scm.pxUserWrite ){ menu_entry("User Database", "setup_user", mprintf("Control how CVSTrac interacts with the %h user " "and password database", g.scm.zName)); } menu_entry("Log File", "setup_log", "Turn the access log file on and off."); menu_entry("Attachments", "setup_attach", "Set the maximum allowable size for attachments."); menu_entry("Abuse Control", "setup_throttle", "Options to control bandwidth abuse and wiki spam."); } menu_entry("Ticket Types", "setup_enum?e=type", "Enumerate the different types of tickets that can be entered into " "the system."); menu_entry("Ticket States", "setup_enum?e=status", "Configure the allowed values for the \"status\" attribute of tickets."); menu_entry("New Tickets Defaults", "setup_newtkt", "Specify the default values assigned to various ticket attributes when " "a new ticket is created."); menu_entry("Subsystem Names", "setup_enum?e=subsys", "List the names of subsystems that can be used in the \"subsystem\" " "attribute of tickets."); menu_entry("User-Defined Fields", "setup_udef", "Create user-defined database columns in the TICKET table"); if( g.okSetup ){ menu_entry("Diff and Filter Programs", "setup_diff", "Specify commands or scripts used to compute the difference between " "two versions of a file and pretty print files."); menu_entry("External Tools", "setup_tools", "Manage tools for processing CVSTrac objects." ); menu_entry("Change Notification", "setup_chng", "Define an external program to run whenever a ticket is created " "or modified."); menu_entry("Customize Style", "setup_style", "Headers, footers, stylesheets, other web page elements."); menu_entry("User Interface", "setup_interface", "Control the user interface functionality." ); menu_entry("Wiki Markup", "setup_markup", "Manage custom Wiki markups" ); menu_entry("Backup & Restore", "setup_backup", "Make a backup copy of the database or restore the database from a " "backup copy."); menu_entry("Timeline & RSS", "setup_timeline", "Set timeline cookie lifetime and RSS \"Time To Live\"."); } @
common_footer(); } /* ** WEBPAGE: /setup_repository */ void setup_repository_page(void){ const char *zRoot, *zOldRoot; const char *zModule, *zOldModule; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** The "r" query parameter is the name of the CVS repository root ** directory. Change it if it has changed. */ zOldRoot = db_config("cvsroot",""); zRoot = P("r"); if( zRoot && strcmp(zOldRoot,zRoot)!=0 ){ db_execute("REPLACE INTO config(name,value) VALUES('cvsroot','%q');", zRoot); zOldRoot = zRoot; db_config(0,0); } /* ** The "m" query parameter is the name of the module within the ** CVS repository that this CVSTrac instance is suppose to track. ** Change it if it has changed. */ zOldModule = db_config("module",""); zModule = P("m"); if( zModule && strcmp(zOldModule,zModule)!=0 ){ db_execute("REPLACE INTO config(name,value) VALUES('module','%q');", zModule); zOldModule = zModule; db_config(0,0); } /* ** The "rrh" query parameter is present if the user presses the ** "Reread Revision History" button. This causes the CVSROOT/history ** file to be reread. Do this with caution as it erases any edits ** to the history that are currently in the database. Only the ** setup user can do this. */ if( P("rrh") ){ common_add_action_item("setup_repository", "Cancel"); common_header("Confirm Reread Of Repository"); @ WARNING! @

@ If you decide to Reconstruct the change history database all @ of your check-ins will be renumbered. This might break links between @ tickets and wiki pages and check-ins. Any edits you may have made @ to check-in messages will be undone as well.

@ @

A safer alternative is to select Rescan which will attempt @ to preserve existing check-in numbers and check-in message changes. @

@ @

In either case, you may want to make a @ backup copy of the database so that you can recover if something @ goes wrong.

@ @
@

@ @ Reconstruct the check-in database from scratch. @

@

@ @ Attempt to reuse existing check-in numbers. @

@

@ @ Do no do anything. @

@
common_footer(); return; } if( P("rrh2") ){ db_execute( "BEGIN;" "DELETE FROM chng WHERE not milestone;" "DELETE FROM filechng;" "DELETE FROM file;" "UPDATE config SET value=0 WHERE name='historysize';" "COMMIT;" "VACUUM;" ); db_config(0,0); history_update(0); } if( P("rrh3") ){ db_execute( "BEGIN;" "DELETE FROM filechng WHERE rowid NOT IN (" "SELECT min(rowid) FROM filechng " "GROUP BY filename, vers||'x'" ");" "DELETE FROM chng WHERE milestone=0 AND cn NOT IN (" "SELECT cn FROM filechng" ");" "UPDATE config SET value=0 WHERE name='historysize';" "COMMIT;" "VACUUM;" ); db_config(0,0); history_update(1); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminRepository"); common_header("Configure Repository"); @

@

@ Enter the full pathname of the root directory of the @ %s(g.scm.zName) repository in the space provided below. if( g.scm.canFilterModules ){ @ If you want to restrict this @ server to see only a subset of the files contained in the @ %s(g.scm.zName) repository @ (for example, if you want to see only one module in a @ repository that contains many unrelated modules) then @ enter a pathname prefix for the files you want to see in the @ second entry box. } @

@

@ @ @ @ if( g.scm.canFilterModules ){ @ @ @ @ } @
%s(g.scm.zName) repository:
Module prefix:

@ @

@ @

@ After changing the %s(g.scm.zName) repository above, you will generally @ want to press the following button to cause the repository history to be @ reread from the new repository. You can also use this button to @ resynchronize the database if a prior read @ failed or if you have manually changed it (always a bad idea). @

@
@

@
common_footer(); } /* ** WEBPAGE: /setup_user */ void setup_user_page(void){ const char *zWPswd, *zOldWPswd; /* The user must be at least the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** The "wpw" query parameter is "yes" if the CVSROOT/passwd file is ** writable and "no" if not. ** Change it if it has changed. */ zOldWPswd = db_config("write_cvs_passwd","yes"); zWPswd = P("wpw"); if( zWPswd && strcmp(zOldWPswd,zWPswd)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('write_cvs_passwd','%q');", zWPswd ); zOldWPswd = zWPswd; db_config(0,0); } /* ** Import users out of the CVSROOT/passwd file if the user pressed ** the Import Users button. Only setup can do this. */ if( P("import_users") && g.scm.pxUserRead ){ g.scm.pxUserRead(); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminUserDatabase"); common_header("Configure User Database Linkage"); @

@

if( g.scm.pxUserWrite ){ @ CVSTrac can update the CVSROOT/passwd file with the usernames and @ passwords of all CVSTrac users. Enable or disable this feature @ below.

@

Write User Changes to CVSROOT/passwd? cgi_optionmenu(0, "wpw", zOldWPswd, "Yes", "yes", "No", "no", 0); @ @

} if( g.scm.pxUserRead ){ @

@ Use the following button to automatically create a CVSTrac user ID @ for every user currently named in CVSROOT/passwd. The new users will @ be given the same access permissions as user "anonymous" plus check-out @ permission and check-in permission if CVS allows the user to write.

@

@
@

} common_footer(); } /* ** WEBPAGE: /setup_log */ void setup_logfile_page(void){ const char *zLog, *zOldLog; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** The "log" query parameter specifies a log file into which a record ** of all HTTP hits is written. Write this value if this has changed. ** Only setup can make this change. */ zOldLog = db_config("logfile",""); zLog = P("log"); if( zLog && strcmp(zOldLog,zLog)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('logfile','%q');", zLog ); zOldLog = zLog; db_config(0,0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminLog"); common_header("Configure Log File"); @

@

@ Enter the name of file into which is written a log of all accesses @ to this server. Leave the entry blank to disable logging: @

@

Log File: @ @

@

common_footer(); } /* ** WEBPAGE: /setup_newtkt */ void setup_newticket_page(void){ char **azResult; const char *zState, *zOldState; const char *zAsgnto, *zOldAsgnto; const char *zType, *zOldType; const char *zPri, *zOldPri; const char *zSev, *zOldSev; /* The user must be at least the administrator in order to see ** this screen. */ login_check_credentials(); if( !g.okAdmin ){ login_needed(); return; } /* ** The "asgnto" query parameter specifies a userid who is assigned to ** all new tickets. Record this value in the configuration table if ** it has changed. */ zOldAsgnto = db_config("assignto",""); zAsgnto = P("asgnto"); if( zAsgnto && strcmp(zOldAsgnto,zAsgnto)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('assignto','%q');", zAsgnto ); zOldAsgnto = zAsgnto; db_config(0,0); } /* ** The "istate" query parameter specifies the initial state for new ** tickets. Record any changes to this value. */ zOldState = db_config("initial_state",""); zState = P("istate"); if( zState && strcmp(zOldState,zState)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('initial_state','%q');", zState ); zOldState = zState; db_config(0,0); } /* ** The "type" query parameter specifies the initial type for new ** tickets. Record any changes to this value. */ zOldType = db_config("dflt_tkt_type","code"); zType = P("type"); if( zType && strcmp(zOldType,zType)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('dflt_tkt_type','%q');", zType ); zOldType = zType; db_config(0,0); } /* ** The "pri" query parameter specifies the initial priority for new ** tickets. Record any changes to this value. */ zOldPri = db_config("dflt_priority","1"); zPri = P("pri"); if( zPri && strcmp(zOldPri,zPri)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('dflt_priority','%q');", zPri ); zOldPri = zPri; db_config(0,0); } /* ** The "sev" query parameter specifies the initial severity for new ** tickets. Record any changes to this value. */ zOldSev = db_config("dflt_severity","1"); zSev = P("sev"); if( zSev && strcmp(zOldSev,zSev)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('dflt_severity','%q');", zSev ); zOldSev = zSev; db_config(0,0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminNewTicket"); common_header("Configure New Ticket Defaults"); @

@

@ Select a user to whom new tickets will be assigned by default:

@ Assigned To: azResult = db_query("SELECT id FROM user UNION SELECT '' ORDER BY id"); cgi_v_optionmenu(0, "asgnto", zOldAsgnto, (const char**)azResult); @

@ @

@ Select the initial state that new tickets are created in:

@ Initial State: cgi_v_optionmenu2(0, "istate", zOldState, (const char**)db_query( "SELECT name, value FROM enums WHERE type='status'")); @

@ @

@ Select the default type for new tickets:

@ Default Type: cgi_v_optionmenu2(0, "type", zOldType, (const char**)db_query( "SELECT name, value FROM enums WHERE type='type'")); @

@ @

@ Select the default priority for new tickets:

@ Default Priority: cgi_optionmenu(0, "pri", zOldPri, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", (char*)0); @

@ @

@ Select the default severity for new tickets:

@ Default Severity: cgi_optionmenu(0, "sev", zOldSev, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", (char*)0); @

@ @

@ @

@

common_footer(); } /* ** WEBPAGE: /setup_interface */ void setup_interface_page(void){ int atkt, ack, tkt, ck; const char *zBrowseUrl; int nCookieLife; /* The user must be at least the administrator in order to see ** this screen. */ login_check_credentials(); if( !g.okAdmin ){ login_needed(); return; } if( P("update") ){ db_execute( "REPLACE INTO config(name,value) VALUES('anon_ticket_linkinfo','%d');" "REPLACE INTO config(name,value) VALUES('anon_checkin_linkinfo','%d');" "REPLACE INTO config(name,value) VALUES('ticket_linkinfo','%d');" "REPLACE INTO config(name,value) VALUES('checkin_linkinfo','%d');" "REPLACE INTO config(name,value) VALUES('browse_url_cookie_life',%d);" "REPLACE INTO config(name,value) VALUES('default_browse_url','%q');", atoi(PD("atkt","0")), atoi(PD("ack","0")), atoi(PD("tkt","0")), atoi(PD("ck","0")), atoi(PD("cl","0")), PD("bu","dir") ); db_config(0, 0); } atkt = atoi(db_config("anon_ticket_linkinfo","0")); ack = atoi(db_config("anon_checkin_linkinfo","0")); tkt = atoi(db_config("ticket_linkinfo","1")); ck = atoi(db_config("checkin_linkinfo","0")); zBrowseUrl = db_config("default_browse_url","dir"); nCookieLife = atoi(db_config("browse_url_cookie_life", "90")); common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminInterface"); common_header("Configure User Interface"); @

@

@ Ticket and check-in/milestone link information enables link tooltips @ in most browsers. For example, @ #1 and @ [1]. While this provides information to the @ user without having to follow a link, it is additional database @ load for the server and can increase the size of the web @ pages considerably. Check-in link information is usually only useful @ if your users put a lot of check-in links within wikis or @ remarks. @

@

@ @
@ @
@ @
@ @

@

@ @ @

@

When browsing the repository there are two ways to list files and @ directories. The Short view is a compact listing combining @ all files and directories into just four columns. The Long view @ shows the most recent repository information for each file.

@


@ @

@ @ @

@

@ Enter number of days browse mode cookie should be kept by users browser. @ This cookie keeps track of user's preferred browse mode across user's @ multiple visits.
@ This applies to all users.
@ Set it to 0 to disable browse mode cookie. @

@

@ Cookie lifetime: @ days @

@ @ @

@

common_footer(); } /* ** Generate a string suitable for inserting into a @

@ common_footer(); } /* ** WEBPAGE: /setup_udef */ void setup_udef_page(void){ int idx, i; const char *zName; const char *zText; /* The user must be at least the administrator in order to see ** this screen. */ login_check_credentials(); if( !g.okAdmin ){ login_needed(); return; } /* Write back results if requested. */ idx = atoi(PD("idx","0")); zName = P("n"); zText = P("x"); if( idx>=1 && idx<=5 && zName && zText ){ char zEnum[20]; char *zName2 = trim_string(zName); char *zDesc2 = trim_string(PD("d","")); bprintf(zEnum,sizeof(zEnum),"extra%d", idx); db_execute("BEGIN"); if( zName2[0] ){ string_to_enum(zEnum, zText); db_execute( "REPLACE INTO config(name,value) VALUES('%s_name','%q');", zEnum, zName2 ); db_execute( "REPLACE INTO config(name,value) VALUES('%s_desc','%q');", zEnum, zDesc2 ); }else{ db_execute("DELETE FROM config WHERE name='%s_name'", zEnum); db_execute("DELETE FROM config WHERE name='%s_desc'", zEnum); db_execute("DELETE FROM enums WHERE type='%s'", zEnum); } db_execute("COMMIT"); db_config(0,0); } /* Genenerate the page. */ common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminUserField"); common_header("Configure User-Defined Fields"); @

@ Five extra columns named "extra1" through "extra5" exist in the @ TICKET table of the database. These columns can be defined for @ application specific use using this configuration page. @

@ @

@ Each column is controlled by a separate form below. The column will @ be displayed on ticket reports if and only if its has a non-blank @ display name. User's see the column as its display name, not as @ "extra1". @

@ @

@ Allowed values for the column can be specified in the text box. @ The same format is used here @ as when specifying ticket types, @ ticket states and @ subsystem names. @ There is one allowed value per line. @ The token on the left is the value as it is stored in the database. @ The text that follows is a human-readable description for the meaning @ of the token. A color name for use in reports may optionally appear @ in parentheses after the description. @

@ @

@ The Allowed Values box may be left blank. @ If allowed values are defined for the column, then users will restricted @ to the values specified when changing the value of the column. @ If no allowed values are defined, then the column can be set to @ arbitrary text. @

@ @

@ The Description box may be left blank. @ If a description is provided, then this field may be entered on the @ new ticket page. If no description is given, this field can be modified @ on the edit ticket page but will not appear on the new ticket page. @

for(i=0; i<5; i++){ const char *zOld; char *zAllowed; const char *zDesc; char zEnumName[30]; bprintf(zEnumName,sizeof(zEnumName),"extra%d_name",i+1); zOld = db_config(zEnumName,""); zEnumName[6] = 0; zAllowed = enum_to_string(zEnumName); bprintf(zEnumName,sizeof(zEnumName),"extra%d_desc",i+1); zDesc = db_config(zEnumName,""); @
@

Database column "extra%d(i+1)":

@
@ @ Display Name: @
@ Allowed Values: (Name Desc Color - omit for free text)
@
@ Description: (HTML - Leave blank to omit from new-ticket page)
@
@ @
} common_footer(); } /* ** WEBPAGE: /setup_chng */ void setup_chng_page(void){ const char *zNotify, *zOldNotify; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** The "notify" query parameter is the name of a program or script that ** is run whenever a ticket is created or modified. Modify the notify ** value if it has changed. Only setup can do this. */ zOldNotify = db_config("notify",""); zNotify = P("notify"); if( zNotify && strcmp(zOldNotify,zNotify)!=0 ){ db_execute( "REPLACE INTO config(name,value) VALUES('notify','%q');", zNotify ); zOldNotify = zNotify; db_config(0,0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminNotification"); common_header("Configure Ticket Change Notification"); @

@

@ Enter a shell command to run whenever a ticket is @ created or modified. The following substitutions are made @ on the string before it is passed to /bin/sh: @ @ @
@ Important Security Note @ @

Be sure to enclose all text substitutions in single-quotes. @ (ex '%%d') Otherwise, a user could cause arbitrary shell @ commands to be run on your system.

@ @

Text is stripped of all single-quotes and backslashs before it is @ substituted, so if the substitution is itself enclosed in single-quotes, @ it will always be treated as a single token by the shell.

@ @

For best security, use only the %%n substitution and have @ a Tcl or Perl script extract other fields directly from the database.

@
@ @
@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
%%aUserID of the person the ticket is assigned to
%%AE-mail address of person assigned to
%%cContact information for the originator
%%dThe description, Wiki format
%%DThe description, HTML format
%%nThe ticket number
%%pThe project name
%%rThe remarks section, Wiki format
%%RThe remarks section, HTML format
%%sThe status of the ticket
%%tThe title of the ticket
%%uUserID of the person who made this change
%%wUserID of the originator of the ticket
%%yType of ticket
%%fFirst TKTCHNG rowid of change set; zero if new record
%%lLast TKTCHNG rowid of change set; zero if new record
%%hattacHment number if change is a new attachment; zero otherwise
%%1First user-defined field
%%2Second user-defined field
%%3Third user-defined field
%%4Fourth user-defined field
%%5Fifth user-defined field
%%%%The literal character "%%"
@
@ @ @ @
@

common_footer(); } /* ** WEBPAGE: /setup_diff */ void setup_diff_page(void){ const char *zDiff, *zOldDiff; const char *zList, *zOldList; const char *zFilter, *zOldFilter; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** The "diff" query parameter is the name of a program or script that ** is run whenever a ticket is created or modified. Modify the filediff ** value if it has changed. Only setup can do this. */ zOldDiff = db_config("filediff",""); zDiff = P("diff"); if( zDiff && strcmp(zOldDiff,zDiff)!=0 ){ if( zDiff[0] ){ db_execute( "REPLACE INTO config(name,value) VALUES('filediff','%q');", zDiff ); }else{ db_execute("DELETE FROM config WHERE name='filediff'"); } zOldDiff = zDiff; db_config(0,0); } /* ** The "list" query parameter is the name of a program or script that ** is run whenever a ticket is created or modified. Modify the filelist ** value if it has changed. Only setup can do this. */ zOldList = db_config("filelist",""); zList = P("list"); if( zList && strcmp(zOldList,zList)!=0 ){ if( zList[0] ){ db_execute( "REPLACE INTO config(name,value) VALUES('filelist','%q');", zList ); }else{ db_execute("DELETE FROM config WHERE name='filelist'"); } zOldList = zList; db_config(0,0); } /* ** The "filter" query parameter is the name of a program or script that any ** files get filtered through for HTML markup. */ zOldFilter = db_config("filefilter",""); zFilter = P("filter"); if( zFilter && strcmp(zOldFilter,zFilter)!=0 ){ if( zFilter[0] ){ db_execute( "REPLACE INTO config(name,value) VALUES('filefilter','%q');", zFilter ); }else{ db_execute("DELETE FROM config WHERE name='filefilter'"); } zOldFilter = zFilter; db_config(0,0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminFilter"); common_header("Configure Source Code Diff Program"); @
@

File Diff

@

Enter a shell command to run in order to compute the difference between @ two versions of the same file. The output can be either plain text @ or HTML. If HTML, then the first non-whitespace character of output @ should be a "<". Otherwise the output will be assumed to be plain text.

@ @ @
@ Important Security Note @ @

Be sure to enclose the substitutions in single-quotes. @ (examples: '%%F' or '%%V2') @ Otherwise, a user who can check in new files @ (with unusual names) can cause arbitrary shell @ commands to be run on your system.

@ @

CVSTrac will not attempt to diff a file whose name contains a @ single-quote or backslash @ so if the substitution is itself enclosed in single-quotes, it will always @ be treated as a single token by the shell.

@
@ @

The following substitutions are made prior to executing the program:

@ @
@ @ if( !strcmp(g.scm.zSCM,"cvs") ){ @ }else{ @ } @ @ @ @ @ @
%%FThe name of the RCS file to be diffed. This is a full @ pathname including the ",v" suffix.The name of the file to be diffed.
%%V1The oldest version to be diffed
%%V2The newest version to be diffed
%%RPPath to repository
%%%%The literal character "%%"
@
@ @ @ @ @

If you leave the above entry blank, the following command is used:

@ @
  if( !strcmp(g.scm.zSCM,"cvs") ){
    @ rcsdiff -q -r'%%V1' -r'%%V2' -u '%%F'
  }else{
    @ svnlook diff -r '%%V2' '%%RP'
  }
  @ 
@
@

@
@
@

File List

@

Enter below a shell command to run in order to list the content @ of a single version of a file as a diff (i.e. for the first @ revision of a file). The output can be either plain text @ or HTML. If HTML, then the first non-whitespace character of output @ should be a "<". Otherwise the output will be assumed to be plain text.

@ @

This command is used to show the content @ of files that are newly added to the repository.

@ @

The following substitutions are made prior to executing the program:

@ @
@ @ if( !strcmp(g.scm.zSCM,"cvs") ){ @ }else{ @ } @ @ @ @ @
%%FThe name of the RCS file to be listed. This is a full @ pathname including the ",v" suffix.The name of the file to be listed.
%%VThe version to be listed
%%RPPath to repository
%%%%The literal character "%%"
@
@ @ @ @ @

If you leave the above entry blank, the following command is used:

@ @
  if( !strcmp(g.scm.zSCM,"cvs") ){
    @ co -q -p'%%V' '%%F' | diff -c /dev/null -
  }else{
    @ svnlook cat -r '%%V' '%%RP' '%%F'
  }
  @ 
@
@
@

@
@

File Filter

@

Enter below a shell command to run in order to filter the contents @ of a single version of a file. The filter should expect the file contents @ on standard input. The output can be either plain text @ or HTML. If HTML, then the first non-whitespace character of output @ should be a "<". Otherwise the output will be assumed to be plain text.

@ @

This command is used to show the content of files

@ @

The following substitutions are made prior to executing the program:

@ @
@ @ if( !strcmp(g.scm.zSCM,"cvs") ){ @ }else{ @ } @ @ @ @ @
%%FThe name of the file to be diffed. This is a relative @ pathname intended for display and content detection purposes.The name of the file to be diffed.
%%VThe version to be listed
%%RPPath to repository
%%%%The literal character "%%"
@
@ @ @ @ @

If you leave the above entry blank, output will simply be wrapped with @ HTML <PRE> tags and encoded as simple HTML. @

@

common_footer(); } /* ** WEBPAGE: /setup_style */ void setup_style_page(void){ const char *zHeader, *zFooter; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** If both "header" and "footer" query parameters are present, then ** change the header and footer to the values of those parameters. ** Only the setup user can do this. */ if( P("ok") && P("header") && P("footer") ){ db_execute( "REPLACE INTO config VALUES('header','%q');" "REPLACE INTO config VALUES('footer','%q');", trim_string(P("header")), trim_string(P("footer")) ); db_config(0,0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminStyle"); if( attachment_max()>0 ){ common_add_action_item("attach_add?tn=0", "Attach"); } common_add_action_item("setup_style", "Cancel"); common_header("Configure Style"); @

@ Enter HTML used for the header and footer of every page. @ If you leave these entries blank, default headers and/or footers @ are used. If you enter a filename (beginning with a "/" character) @ instead of HTML text, then the @ file is read at runtime and used for the header or footer.

@ @

@ You may attach files to this page which can then be referenced from within @ your custom header/footer or from other pages. For example, stylesheets, @ JavaScript files, logos, icons, etc can all be attached. These attachments may @ be referenced directly by filename (i.e. /filename.png) @ rather than using attach_get/89/filename.png links.

@ @

Substitutions are made within the header and footer text. These @ substitutions are made to the HTML regardless of whether the HTML @ is entered directly below or is read from a file.

@ @
@ @ @ @ @ @ @
%%NThe name of the project
%%TThe title of the current page
%%VThe version number of CVSTrac
%%BCVSTrac base URL
%%%%The literal character "%%"
@
@ @

@

zHeader = db_config("header",""); zFooter = db_config("footer",""); /* user wants to restore the defaults */ if( P("def") ){ zHeader = HEADER; zFooter = FOOTER; } @ Header:
@
@ Footer:
@
@ @ @ @
@

attachment_html("0","",""); common_footer(); } /* ** Make a copy of file zFrom into file zTo. If any errors occur, ** return a pointer to a string describing the error. */ static const char *file_copy(const char *zFrom, const char *zTo){ FILE *pIn, *pOut; int n; long long int total = 0; char zBuf[10240]; pIn = fopen(zFrom, "r"); if( pIn==0 ){ return mprintf( "Unable to copy files - cannot open \"%h\" for reading.", zFrom ); } unlink(zTo); pOut = fopen(zTo, "w"); if( pOut==0 ){ fclose(pIn); return mprintf( "Unable to copy files - cannot open \"%h\" for writing.", zTo ); } while( (n = fread(zBuf, 1, sizeof(zBuf), pIn))>0 ){ if( fwrite(zBuf, 1, n, pOut) @ Enter the maximum attachment size below. If you enter a size of @ zero, attachments are disallowed. @

@ @

@

@ Maximum attachment size in kilobytes: @ @ @
@

common_footer(); } /* ** WEBPAGE: /setup_throttle */ void setup_throttle_page(void){ int mxHit = atoi(db_config("throttle","0")); int nf = atoi(db_config("nofollow_link","0")); int cp = atoi(db_config("enable_captcha","0")); int lnk = atoi(db_config("max_links_per_edit","0")); int mscore = atoi(db_config("keywords_max_score","0")); const char *zKeys = db_config("keywords",""); /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } if( P("sz") && atoi(P("sz"))!=mxHit ){ mxHit = atoi(P("sz")); db_execute("REPLACE INTO config VALUES('throttle',%d)", mxHit); db_config(0, 0); } if( P("nf") && atoi(P("nf"))!=nf ){ nf = atoi(P("nf")); db_execute("REPLACE INTO config VALUES('nofollow_link',%d)", nf); db_config(0, 0); } if( P("cp") && atoi(P("cp"))!=cp ){ cp = atoi(P("cp")); db_execute("REPLACE INTO config VALUES('enable_captcha',%d)", cp); db_config(0, 0); } if( P("lnk") && atoi(P("lnk"))!=lnk ){ lnk = atoi(P("lnk")); db_execute("REPLACE INTO config VALUES('max_links_per_edit',%d)", lnk); db_config(0, 0); } if( P("mscore") && atoi(P("mscore"))!=mscore ){ mscore = atoi(P("mscore")); db_execute("REPLACE INTO config VALUES('keywords_max_score',%d)", mscore); db_config(0, 0); } if( P("keys") && strcmp(zKeys,PD("keys","")) ){ zKeys = P("keys"); db_execute("REPLACE INTO config VALUES('keywords','%q')", zKeys); db_config(0, 0); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminAbuse"); common_header("Abuse Controls"); @

Set Maximum Anonymous Hits Per Hour

@

@ Enter the limit on the number of anonymous accesses from the same @ IP address that can occur within one hour. Enter zero to disable @ the limiter. @

@ @

@

@ Maximum hits per hour: @ @ @
@

@ @

@ The limiter works by maintain a table in the database (the ACCESS_LOAD @ table) that records the time of last access and a "load" for each @ IP address. The load reduces exponentially with a half-life of @ one hour. Each new access increases the load by 1.0. When the @ load exceeds the threshold above, the load automatically doubles and @ the client is bounced to the captcha @ page. After this redirection happens a @ few times, the user is denied access until the load decreases @ below the threshold. If the user passes the @ captcha test, a cookie is set. @

@ @

@ When the limiter is enabled, the captcha @ page is also used to screen users before they try to do anything @ that might change the database (create a new ticket, @ change a wiki page, etc). This @ feature is intended to block automated wiki spam. @ @

@ Any attempt to access the page named "stopper" (reachable from @ honeypot) automatically increases @ the load to twice the threshold. There are hyperlinks to the @ honeypot on every page. The idea is to trick spiders into visiting @ this honeypot so that they can have their access terminated quickly. @

@ @

@ The limiter and the honeypot only work for users that are not @ logged in - anonymous users. Users who are logged in can visit @ any page (including the honeypot) as often as they want and will @ never be denied access. The limiter (but not the honeypot) is also @ disabled for any user with a valid captcha @ cookie. @

@ @

A summary of the Access Log is available @ separately.

@
@

Captcha

@
@

@ By turning on this option, anonymous users will be required to pass a @ simple captcha @ test before being allowed to change content (tickets, wiki, etc). Passing @ the test will set a cookie on the browser. Too many failures to pass @ the test will trigger the throttler and lock the users IP address out. @ Note that the rate limiter has to be enabled (non-zero) for this option @ to be available. @

@

@ @

@ @
@
@

External Links

@
@

@ By turning on this option, all links to external sites are tagged as @ "nofollow". This provides a hint to search engines to ignore such links @ and reduces the value of wiki spam. However, this may be of limited use @ since wiki spammers aren't always smart enough to notice that they're @ wasting their time. @

@

@ @

@ @
@ @
@

@ Wiki spam generally works by inserting large numbers of links in a @ single page edit. A simple way to prevent this is to simply impose a @ maximum number of new external links in a single wiki edit. @ A value of zero will disable this option. @

@

@ Maximum external links per Wiki edit: @ @

@ @
@
@

Keyword Filtering

@
@

@ Enter a space-separated list of keywords. All wiki edits will be @ checked against this list and, if the maximum score is exceeded, @ the change will be denied. The scoring algorithm uses the standard @ CVSTrac text search() function (where each matched @ keyword scores from 6 to 10 points). Repeating a keyword in the @ list will cause it to score higher. @

@

cgi_text("mscore", 0, 0, 0, 0, 5, 8, 1, mprintf("%d",mscore), "Maximum keyword score"); @

@

@

Forbidden Keywords

@ @

@ @
common_footer(); } /* ** WEBPAGE: /setup_markupedit */ void setup_markupedit_page(void){ const char *zMarkup = PD("m",""); const char *zType = PD("t","0"); const char *zFormat = PD("f",""); const char *zDescription = PD("d",""); int delete = atoi(PD("del","0")); /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } common_add_nav_item("setup", "Main Setup Menu"); common_add_action_item("setup_markup", "Cancel"); common_add_action_item(mprintf("setup_markupedit?m=%h&del=1",zMarkup), "Delete"); common_add_help_item("CvstracAdminMarkup"); common_header("Custom Wiki Markup"); if( P("can") ){ cgi_redirect("setup_markup"); return; }else if( P("ok") ){ /* delete it */ db_execute("DELETE FROM markup WHERE markup='%q';", zMarkup); cgi_redirect("setup_markup"); return; }else if( delete && zMarkup[0] ){ @

Are you sure you want to delete markup %h(zMarkup)?

@ @
@ @ @ @
common_footer(); return; } if( P("u") ){ if( zMarkup[0] && zType[0] && zFormat[0] ) { /* update database and bounce back to listing page. If the ** description is empty, we'll survive (and wing it). */ db_execute("REPLACE INTO markup(markup,type,formatter,description) " "VALUES('%q',%d,'%q','%q');", zMarkup, atoi(zType), zFormat, zDescription); } cgi_redirect("setup_markup"); return; } if( zMarkup[0] ){ /* grab values from database, if available */ char **az = db_query("SELECT type, formatter, description " "FROM markup WHERE markup='%q';", zMarkup); if( az && az[0] && az[1] && az[2] ){ zType = az[0]; zFormat = az[1]; zDescription = az[2]; } } @
@ Markup Name: cgi_optionmenu(0,"t",zType, "Markup","0", "Block","2", "Program Markup","1", "Program Block","3", NULL); @
Formatter:
@
@ Description:
@
@ @ @
@ @ @
@ Important Security Note @ @

Program formatters execute external scripts and programs and @ improper configuration may result in a compromised server.

@ @

Be sure to enclose all text substitutions in single-quotes. @ (ex '%%k') Otherwise, a user could cause arbitrary shell @ commands to be run on your system.

@ @

Text is stripped of all single-quotes and backslashs before it is @ substituted, so if the substitution is itself enclosed in single-quotes, @ it will always be treated as a single token by the shell.

@
@ @ The following substitutions are made on the custom markup: @
@ @ @ @ @ @ @ @ @ @ @
%%mmarkup name
%%kmarkup key
%%amarkup arguments
%%xmarkup arguments or, if empty, key
%%bmarkup block
%%r%s(g.scm.zName) repository root
%%nCVSTrac project name
%%uCurrent user
%%cUser capabilities
@
@ @ Additionally, external programs have some or all the following @ environment variables defined:
@ REQUEST_METHOD, GATEWAY_INTERFACE, REQUEST_URI, PATH_INFO, @ QUERY_STRING, REMOTE_ADDR, HTTP_USER_AGENT, CONTENT_LENGTH, @ HTTP_REFERER, HTTP_HOST, CONTENT_TYPE, HTTP_COOKIE @
@ @

Notes

@
    @
  • The markup name is the wiki formatting tag. i.e. a markup named @ echo would be invoked using {echo: key args}
  • @
  • Changing the name of an existing markup may break existing @ wiki pages
  • @
  • "Markup" markups are simple string substitutions and are handled @ directly by CVSTrac
  • @
  • "Block" markups are paired {markup} and {endmarkup} which get @ all the text in between as arguments (%a), with no key.
  • @
  • "Program" markups are handled by running external scripts and @ programs. These are more flexible, but there are security risks and @ too many may slow down page creation. A Program Markup gets the @ arguments on the command line while a Program Block also gets the block @ from standard input. Both forms should write HTML to standard output
  • @
  • The Description field is used when enumerating the list of available @ custom markups using the {markups} tag. This is included in pages @ such as WikiFormatting in order to @ document server-specific markups.
  • @
common_footer(); } /* ** WEBPAGE: /setup_markup */ void setup_markup_page(void){ int j; char **az; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } common_add_nav_item("setup", "Main Setup Menu"); common_add_action_item("setup_markupedit", "Add Markup"); common_add_help_item("CvstracAdminMarkup"); common_header("Custom Wiki Markup"); az = db_query("SELECT markup, description FROM markup ORDER BY markup;"); if( az && az[0] ){ @

Custom Markup Rules

@
for(j=0; az[j]; j+=2){ @
%h(az[j])
if( az[j+1] && az[j+1][0] ){ /* this markup has a description, output it. */ @
output_formatted(az[j+1],NULL); @
}else{ @
(no description)
} } @
} common_footer(); } /* ** WEBPAGE: /setup_toolsedit */ void setup_toolsedit_page(void){ const char *zTool = PD("t",""); const char *zObject = PD("o",""); const char *zCommand = PD("c",""); const char *zDescription = PD("d",""); const char *zPerms = PD("p","as"); int delete = atoi(PD("del","0")); /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } common_add_nav_item("setup", "Main Setup Menu"); common_add_action_item("setup_tools", "Cancel"); common_add_action_item(mprintf("setup_toolsedit?t=%h&del=1",zTool), "Delete"); common_header("External Tools"); if( P("can") ){ cgi_redirect("setup_tools"); return; }else if( P("ok") ){ /* delete it */ db_execute("DELETE FROM tool WHERE name='%q';", zTool); cgi_redirect("setup_tools"); return; }else if( delete && zTool[0] ){ @

Are you sure you want to delete tool %h(zTool)?

@ @
@ @ @ @
common_footer(); return; } if( P("u") ){ if( zTool[0] && zPerms[0] && zObject[0] && zCommand[0] ) { /* update database and bounce back to listing page. If the ** description is empty, we'll survive (and wing it). */ db_execute("REPLACE INTO tool(name,perms,object,command,description) " "VALUES('%q','%q','%q','%q','%q');", zTool, zPerms, zObject, zCommand, zDescription); } cgi_redirect("setup_tools"); } if( zTool[0] ){ /* grab values from database, if available */ char **az = db_query("SELECT perms, object, command, description " "FROM tool WHERE name='%q';", zTool); if( az && az[0] && az[1] && az[2] && az[3] ){ zPerms = az[0]; zObject = az[1]; zCommand = az[2]; zDescription = az[3]; } } @
@ Tool Name: cgi_optionmenu(0,"o",zObject, "File","file", "Wiki","wiki", "Ticket","tkt", "Check-in","chng", "Milestone","ms", "Report", "rpt", "Directory", "dir", NULL); @
Required Permissions: @
@
Command-line:
@
@ Description:
@
@ @ @
@ @ @
@ Important Security Note @ @

External scripts and programs and @ improper configuration may result in a compromised server.

@ @

Be sure to enclose all text substitutions in single-quotes. @ (ex '%%k') Otherwise, a user could cause arbitrary shell @ commands to be run on your system.

@ @

Text is stripped of all single-quotes and backslashs before it is @ substituted, so if the substitution is itself enclosed in single-quotes, @ it will always be treated as a single token by the shell.

@ @

Each tool can have a minimum permission set defined. See @ Users for the full list.

@
@ @ The following substitutions are available to all external tools: @
@ @ @ @ @ @ @ @ @
%%RP%s(g.scm.zName) repository root
%%PCVSTrac project name
%%BServer base URL
%%UCurrent user
%%UCUser capabilities
%%NCurrent epoch time
%%TName of tool
@
@ @ File tools have the following substitutions available: @
@ @ @ @ @
%%FFilename
%%V1First version number
%%V2Second version number (i.e. diff)
@
@ @ Directory tools have the following substitutions available: @
@ @ @
%%FDirectory pathname
@
@ @ Ticket tools have the following substitutions available: @
@ @ @
%%TNTicket number
@
@ @ Wiki tools have the following substitutions available: @
@ @ @ @ @ @
%%WWiki page name
%%T1First timestamp of wiki page
%%T2Second timestamp of wiki page (i.e. diff) @
%%CTemporary file containing content
@
@ @ Check-in tools have the following substitutions available: @
@ @ @ @
%%CNCheck-in number
%%CTemporary file containing message
@
@ @ Milestone tools have the following substitutions available: @
@ @ @ @
%%MSMilestone number
%%CTemporary file containing message
@
@ @ Report tools have the following substitutions available: @
@ @ @
%%RNReport number
@
@ @ Additionally, external programs have some or all the following @ environment variables defined:
@ REQUEST_METHOD, GATEWAY_INTERFACE, REQUEST_URI, PATH_INFO, @ QUERY_STRING, REMOTE_ADDR, HTTP_USER_AGENT, CONTENT_LENGTH, @ HTTP_REFERER, HTTP_HOST, CONTENT_TYPE, HTTP_COOKIE @
common_footer(); } /* ** WEBPAGE: /setup_tools */ void setup_tools_page(void){ int j; char **az; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } common_add_nav_item("setup", "Main Setup Menu"); common_add_action_item("setup_toolsedit", "Add Tool"); common_header("External Tools"); az = db_query("SELECT name, description FROM tool ORDER BY name;"); if( az && az[0] ){ @

External Tools

@
for(j=0; az[j]; j+=2){ @
%h(az[j])
if( az[j+1] && az[j+1][0] ){ /* this tool has a description, output it. */ @
output_formatted(az[j+1],NULL); @
}else{ @
(no description)
} } @
} common_footer(); } /* ** WEBPAGE: /setup_backup */ void setup_backup_page(void){ char *zDbName = mprintf("%s.db", g.zName); char *zBuName = mprintf("%s.db.bu", g.zName); const char *zMsg = 0; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } if( P("bkup") ){ db_execute("BEGIN"); zMsg = file_copy(zDbName, zBuName); db_execute("COMMIT"); }else if( P("rstr") ){ db_execute("BEGIN"); zMsg = file_copy(zBuName, zDbName); db_execute("COMMIT"); } common_add_nav_item("setup", "Main Setup Menu"); common_add_help_item("CvstracAdminBackup"); common_header("Backup The Database"); if( zMsg ){ @

%s(zMsg)

} @

@ Use the buttons below to make a safe (atomic) backup or restore @ of the database file. The original database is in the file @ named %h(zDbName) and the backup is in @ %h(zBuName). @

@ @

@ It is always safe to do a backup. The worst that can happen is that @ you can overwrite a prior backup. But a restore can destroy your @ database if the backup copy you are restoring from is incorrect. @ Use caution when doing a restore. @

@ @
@

@

@
common_footer(); } /* ** WEBPAGE: /setup_timeline */ void setup_timeline_page(void){ int nCookieLife; int nTTL; int nRDL; /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } if( P("cl") || P("ttl") || P("rdl") ){ if( P("cl") ){ int nCookieLife = atoi(P("cl")); db_execute("REPLACE INTO config VALUES('timeline_cookie_life',%d)", nCookieLife); } if( P("ttl") ){ int nTTL = atoi(P("ttl")); db_execute("REPLACE INTO config VALUES('rss_ttl',%d)", nTTL); } if( P("rdl") ){ int nRDL = atoi(P("rdl")); db_execute("REPLACE INTO config VALUES('rss_detail_level',%d)", nRDL); } db_config(0, 0); } nCookieLife = atoi(db_config("timeline_cookie_life", "90")); nTTL = atoi(db_config("rss_ttl", "60")); nRDL = atoi(db_config("rss_detail_level", "5")); common_add_nav_item("setup", "Main Setup Menu"); common_header("Timeline & RSS Setup"); @
@

@ Enter number of days timeline cookie should be kept by users browser. @ This cookie keeps timeline settings persistent across users multiple visits.
@ This applies to all users.
@ Set it to 0 to disable timeline cookie. @

@

@ Cookie lifetime: @ days @ @

@
@

@ RSS feed's TTL (Time To Live) tells RSS readers how long a feed should @ be cached before refreshing from the source. Because a refresh @ downloads the entire page, in order to avoid excessive use of @ bandwidth this shouldn't be set too low. Anything lower then 15 @ is probably not a very good idea, while 30-60 is most common. @

@

@ Time To Live: @ minutes @ @

@
@

@ RSS feed's detail level determines how much details will be @ embedded in feed.
@ Higher the detail level, higher the bandwidth usage will be. @

@

@ RSS detail level:
@
@
@
@ @

@
common_footer(); } #if 0 /* TO DO */ /* ** WEB-PAGE: /setup_repair */ void setup_repair_page(void){ /* The user must be the setup user in order to see ** this screen. */ login_check_credentials(); if( !g.okSetup ){ cgi_redirect("setup"); return; } /* ** Change a check-in number. */ cnfrom = atoi(PD("cnfrom"),"0"); cnto = atoi(PD("cnto"),"0"); if( cnfrom>0 && cnto>0 && cnfrom!=cnto ){ const char *zOld; zOld = db_short_query( "SELECT rowid FROM chng WHERE cn=%d", cnfrom ); if( zOld || zOld[0] ){ db_execute( "BEGIN;" "DELETE FROM chng WHERE cn=%d;" "UPDATE chng SET cn=%d WHERE cn=%d;" "UPDATE filechng SET cn=%d WHERE cn=%d;" "UPDATE xref SET cn=%d WHERE cn=%d;" "COMMIT;", cnto, cnto, cnfrom, cnto, cnfrom, cnto, cnfrom ); } } /* ** Remove duplicate entries in the FILECHNG table. Remove check-ins ** from the CHNG table that have no corresponding FILECHNG entries. */ if( P("rmdup") ){ db_execute( "BEGIN;" "DELETE FROM filechng WHERE rowid NOT IN (" "SELECT min(rowid) FROM filechng " "GROUP BY filename, vers||'x'" ");" "DELETE FROM chng WHERE milestone=0 AND cn NOT IN (" "SELECT cn FROM filechng" ");" "COMMIT;" ); } common_add_nav_item("setup", "Main Setup Menu"); common_header("Repair The Database"); @

@ You can use this page to repair damage to a database that was caused @ when the repository was read incorrectly. The problem may @ have resulted from corruption in the %s(g.scm.zName) repository or a system @ malfunction or from a bug in CVSTrac. (All known bugs of this kind have @ been fixed but you never know when a new one might appear.) @

@ @ @

common_footer(); } #endif