/* ** 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/ ** ******************************************************************************* ** ** This file contains code used to browse through the CVS repository. */ #include "config.h" #include "browse.h" #include #include #include #include #include #include /* ** This routine generates an HTML page that describes the complete ** revision history for a single file. */ static void revision_history(const char *zName, int showMilestones){ char **az; int i; const char *zTail; if( zName[0]=='/' ) zName++; /* Be nice to TortoiceCVS */ zTail = strrchr(zName, '/'); if( zTail ) zTail++; /* @

History of /%h(zName)

*/ if( showMilestones ){ common_add_action_item(mprintf("rlog?f=%t",zName), "Omit Milestones"); az = db_query("SELECT filechng.cn, date, vers, nins, ndel, prevvers," " message, user, branch " "FROM filechng, chng " "WHERE filename='%q' AND filechng.cn=chng.cn " "UNION ALL " "SELECT '',date,cn,NULL,NULL,NULL,message,user,branch " "FROM chng " "WHERE milestone=1 " "ORDER BY 2 DESC", zName); } else { common_add_action_item(mprintf("rlog?f=%t&sms=1",zName), "Show Milestones"); az = db_query("SELECT filechng.cn, date, vers, nins, ndel, prevvers," " message, user, branch " "FROM filechng, chng " "WHERE filename='%q' AND filechng.cn=chng.cn " "ORDER BY date DESC", zName); } common_header("History for /%h", zName); cgi_printf("\n"); for(i=0; az[i]; i+=9){ time_t t; struct tm *pTm; char zDate[100]; t = atoi(az[i+1]); pTm = localtime(&t); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm); if( i==0 ){ cgi_printf("\n" " \n" "\n"); } if( i%2 ){ cgi_printf("\n",BG4); }else{ cgi_printf("\n"); } cgi_printf("\n",zDate); if( az[i][0]==0 ){ cgi_printf("\n"); if( az[i+8] && az[i+8][0] ){ cgi_printf("\n",zName,az[i+2],printable_vers(az[i+2])); if( az[i+8] && az[i+8][0] ){ cgi_printf("\n"); } if( i==0 ){ cgi_printf("\n"); } cgi_printf("
DateVersionDescription
%s\n"); common_icon("box"); cgi_printf("\n" "Milestone\n",BG5); output_chng(atoi(az[i+2])); cgi_printf(" on branch %h:\n",az[i+8]); }else{ cgi_printf("\n" "Milestone\n"); output_chng(atoi(az[i+2])); } }else{ cgi_printf("  \n" "\n" " %h\n" "  Check-in\n",BG5); output_chng(atoi(az[i])); cgi_printf(" on branch %h:\n",az[i+8]); }else{ cgi_printf("Check-in\n"); output_chng(atoi(az[i])); cgi_printf(":\n"); } } output_formatted(az[i+6], 0); cgi_printf(" By %z.\n",format_user(az[i+7])); if( az[i][0]!=0 ){ /* Can't diff a Milestone */ if( g.okCheckout && az[i+5] && az[i+5][0] ){ cgi_printf("\n" "(diff)\n",zName,az[i+5],az[i+2]); } } cgi_printf("
Nothing is known about this file
\n"); } /* ** Adds all appropriate action bar links for file tools */ static void add_file_tools( const char *zExcept, const char *zFile, const char *zVers1, const char *zVers2 ){ int i; char *zLink; char **azTools; db_add_functions(); azTools = db_query("SELECT tool.name FROM tool,user " "WHERE tool.object='file' AND user.id='%q' " " AND cap_and(tool.perms,user.capabilities)!=''", g.zUser); for(i=0; azTools[i]; i++){ if( zExcept && 0==strcmp(zExcept,azTools[i]) ) continue; zLink = mprintf("filetool?t=%T&f=%T%s%T%s%T", azTools[i], zFile, zVers1?"&v1=":"", zVers1?zVers1:"", zVers2?"&v2=":"", zVers2?zVers2:""); common_add_action_item(zLink, azTools[i]); } } /* ** Adds all appropriate action bar links for directory tools */ static void add_dir_tools( const char *zExcept, const char *zDir ){ int i; char *zLink; char **azTools; db_add_functions(); azTools = db_query("SELECT tool.name FROM tool,user " "WHERE tool.object='dir' AND user.id='%q' " " AND cap_and(tool.perms,user.capabilities)!=''", g.zUser); for(i=0; azTools[i]; i++){ if( zExcept && 0==strcmp(zExcept,azTools[i]) ) continue; zLink = mprintf("dtool?t=%T&d=%T", azTools[i], zDir); common_add_action_item(zLink, azTools[i]); } } /* ** WEBPAGE: /dtool ** ** Execute an external tool on a given directory */ void dirtool(void){ const char *zDir = PD("d",""); const char *zTool = P("t"); char *zDirUrl; char *zAction; const char *azSubst[32]; int n = 0; if( zDir==0 || zTool==0 ) cgi_redirect("index"); login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); history_update(0); zDirUrl = mprintf("%T?d=%T", default_browse_url(), zDir); zAction = db_short_query("SELECT command FROM tool " "WHERE name='%q'", zTool); if( zAction==0 || zAction[0]==0 ) cgi_redirect(zDirUrl); common_standard_menu(0, "search?f=1"); common_add_action_item(zDirUrl,"Directory"); add_dir_tools(zTool,zDir); common_header("%s for /%h", zTool, zDir); azSubst[n++] = "F"; azSubst[n++] = quotable_string(zDir); azSubst[n++] = 0; n = execute_tool(zTool,zAction,0,azSubst); free(zAction); if( n<=0 ){ cgi_redirect(zDirUrl); } common_footer(); } /* ** WEBPAGE: /filetool ** ** Execute an external tool on a given target */ void filetool(void){ const char *zFile = P("f"); const char *zVers1 = PD("v1",""); const char *zVers2 = PD("v2",""); const char *zTool = P("t"); char *zAction; const char *azSubst[32]; int n = 0; if( zFile==0 || zTool==0 ) cgi_redirect("index"); login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); history_update(0); zAction = db_short_query("SELECT command FROM tool " "WHERE name='%q'", zTool); if( zAction==0 || zAction[0]==0 ) cgi_redirect("index"); common_standard_menu(0, "search?f=1"); common_add_action_item(mprintf("rlog?f=%T", zFile), "History"); add_file_tools(zTool,zFile,zVers1,zVers2); common_header("%s for /%h", zTool, zFile); cgi_printf("%h\n",zFile,zFile); if( zVers1 ){ char *zFV = mprintf("fileview?f=%T&v=%T", zFile, zVers1); cgi_printf("%h
\n",zFV,zVers1); } azSubst[n++] = "F"; azSubst[n++] = quotable_string(zFile); azSubst[n++] = "V1"; azSubst[n++] = quotable_string(zVers1); azSubst[n++] = "V2"; azSubst[n++] = quotable_string(zVers2); azSubst[n++] = 0; n = execute_tool(zTool,zAction,0,azSubst); free(zAction); if( n<=0 ){ cgi_redirect(mprintf("rlog?f=%T",zFile)); } common_footer(); } /* ** WEBPAGE: /rlog ** ** This page lists the revision history for a single file. Hyperlinks ** allow the file to be diffed or annotated. */ void browse_rlog(void){ char *zDir, *z; int showMilestones; const char *zFile; login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); common_standard_menu(0, "search?f=1"); showMilestones = atoi(PD("sms","0")); history_update(0); zFile = PD("f",""); /* Make sure we always have '/' in zFile, otherwise link to parent ** directory won't work for file in repository root. */ if( strrchr(zFile, '/') ){ zDir = mprintf("%T?d=%T", default_browse_url(), zFile); }else{ zDir = mprintf("%T?d=/%T", default_browse_url(), zFile); } z = strrchr(zDir, '/' ); if( z ){ *z = 0;} common_add_action_item(zDir, "Directory"); add_file_tools(0,zFile,0,0); common_add_help_item("CvstracFileHistory"); revision_history(zFile, showMilestones); common_footer(); } /* ** WEBPAGE: /filediff ** ** Show the differences between two versions of a file */ void browse_filediff(void){ const char *zFile = P("f"); const char *zV1 = P("v1"); const char *zV2 = P("v2"); login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); if( zFile==0 || zV1==0 || zV2==0 ){ cgi_redirect("index"); return; } common_standard_menu(0, "search?f=1"); common_add_action_item(mprintf("rlog?f=%T", zFile), "History"); add_file_tools(0,zFile,zV1,zV2); common_add_help_item("CvstracFileHistory"); common_header("Difference in %h versions %h and %h", zFile, printable_vers(zV1), printable_vers(zV2)); if( diff_versions(zV1, zV2, zFile) ){ cgi_printf("Diff failed!\n"); } common_footer(); } /* ** WEBPAGE: /dir ** ** List all of the repository files in a single directory. */ void browse_dir(void){ const char *zName; char *zDir; char *zBase; char **az; int i, j; int n; int nRow; const char *zCookieName; int nCookieLife; login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); history_update(0); common_standard_menu("dir", "search?f=1"); /* P("sc") is set only when user explicitly switches to Long/Short view, ** via action bar link. In that case we make that users preference ** persistent via cookie. */ if( P("sc") ){ zCookieName = mprintf("%t_browse_url",g.zName); nCookieLife = 86400*atoi(db_config("browse_url_cookie_life","90")); if( nCookieLife ){ cgi_set_cookie(zCookieName, "dir", 0, nCookieLife); } } zName = PD("d",""); if( zName==0 ){ zName = ""; } common_add_help_item("CvstracBrowse"); if( zName[0] ){ common_add_action_item( mprintf("timeline?x=1&c=2&dm=1&px=%h",zName), "Activity" ); } add_dir_tools(0,zName); zDir = mprintf("%s", zName); zBase = strrchr(zDir, '/'); if( zBase==0 ){ zBase = zDir; zDir = ""; }else{ *zBase = 0; zBase++; } if( zName && zName[0] ){ /* this looks like navigation, but it's relative to the current page */ common_add_action_item("dir", "Top"); common_add_action_item(mprintf("dir?d=%T",zDir), "Up"); common_add_action_item(mprintf("dirview?d=%T&sc=1",zName), "Long"); }else{ common_add_action_item("dirview?sc=1","Long"); } az = db_query("SELECT base, isdir FROM file WHERE dir='%q' ORDER BY base", zName); for(n=0; az[n*2]; n++){} if( zName[0] ) n++; nRow = (n+3)/4; if( zName[0] ){ zName = mprintf("%s/",zName); } common_header("Directory /%h", zName); /* @

Contents of directory /%h(zName)

*/ cgi_printf("\n" "\n"); for(i=j=0; i<4; i++){ cgi_printf("\n"); } cgi_printf("
\n"); n = 0; if( i==0 && zName[0] ){ cgi_printf("\n",zDir); common_icon("backup"); cgi_printf(" ..
\n",zDir); n++; } while( n\n",zName,az[j]); common_icon("dir"); cgi_printf(" %h/
\n",zName,az[j],az[j]); }else{ char *zIcon; char *zFilename = mprintf("%s%s", zName, az[j]); if(is_file_available(zFilename)){ zIcon = "file"; }else{ zIcon = "del"; } if( zFilename!=0 ) free(zFilename); cgi_printf("\n",zName,az[j]); common_icon(zIcon); cgi_printf(" %h
\n",zName,az[j],az[j]); } n++; j += 2; } cgi_printf("
\n"); common_footer(); } /* ** This routine is used to represent age of files in english text. ** For example "1 week", "3 days", etc. ** It takes integer representing unix time of file's last modification and ** calculates it's age relative to current time. */ static char *file_age_to_text(int nModified){ int nAge, n; int nYear = 31536000; /* Number of seconds in a year */ int nMonth = 2592000; /* Number of seconds in a month */ int nWeek = 604800; /* Number of seconds in a week */ int nDay = 86400; /* Number of seconds in a day */ if( nModified<=0 ){ /* FIXME: some error handling would be nice here */ return NULL; } nAge = (int)time(0)-nModified; if( nAge<0 ){ /* FIXME: some error handling would be nice here */ return NULL; } if( (n = nAge/nYear)>1 ){ return mprintf("%d years", n); }else if( (n = nAge/nMonth)>1 ){ return mprintf("%d months", n); }else if( (n = nAge/nWeek)>1 ){ return mprintf("%d weeks", n); }else if( (n = nAge/nDay)>1 ){ return mprintf("%d days", n); }else if( (n = nAge/3600)>1 ){ return mprintf("%d hours", n); }else{ n = nAge/60; if( n<=1 ){ return mprintf("1 minute"); }else{ return mprintf("%d minutes", n); } } } static void column_header( const char *zNameNS, char zFld, const char *zField, const char *zColumn ){ int set = (zFld==zField[0]); int desc = P("desc")!=0; const char *zDesc = set ? (desc ? "" : "&desc" ) : ""; if(set){ cgi_printf("%h\n",zNameNS,zField,zDesc,zColumn); } /* ** Output a long directory row */ static void row_content( const char *zName, int nCol, const char **az ){ if( (nCol%2)==0 ){ cgi_printf("\n",BG4); }else{ cgi_printf("\n"); } if( atoi(az[0])==1 ){ cgi_printf("\n" "\n",zName,az[1]); common_icon("dir"); cgi_printf(" %h/\n" "%h\n" "\n",zName,az[1],az[1],file_age_to_text(atoi(az[5]))); }else{ cgi_printf("\n" "\n",zName,az[1]); if( atoi(az[3])==2 ){ common_icon("del"); }else{ common_icon("file"); } cgi_printf(" %h\n" "\n" "\n" "%h \n" "%z\n" "%h\n" "\n",zName,az[1],az[1],zName,az[1],az[2],printable_vers(az[2]),format_user(az[4]),file_age_to_text( atoi(az[5]) )); if( output_trim_message(az[6], MN_CKIN_MSG, MX_CKIN_MSG) ){ output_formatted(az[6], 0); cgi_printf(" [...]\n"); }else{ output_formatted(az[6], 0); } cgi_printf("\n"); } cgi_printf("\n"); } /* ** WEBPAGE: /dirview ** ** This is a "long view" version of /dir page. ** List all of the repository files in a single directory and display ** information about their last change. */ void browse_dirview(void){ const char *zName; const char *zNameNS; /* NoSlash */ char *zDir; char *zBase; char **az; int i; const char *zCookieName; int nCookieLife; char *zDesc; char *zOrderBy = "1 DESC, 2"; const char *z; char zFld = 0; login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); history_update(0); common_standard_menu("dirview", "search?f=1"); /* P("sc") is set only when user explicitly switches to Long/Short view, ** via action bar link. In that case we make that users preference ** persistent via cookie. */ if( P("sc") ){ zCookieName = mprintf("%t_browse_url",g.zName); nCookieLife = 86400*atoi(db_config("browse_url_cookie_life","90")); if( nCookieLife ){ cgi_set_cookie(zCookieName, "dirview", 0, nCookieLife); } } zName = PD("d",""); if( zName==0 ){ zName = ""; } if( zName[0] ){ common_add_action_item( mprintf("timeline?x=1&c=2&dm=1&px=%T",zName), "Activity" ); } add_dir_tools(0,zName); zDir = mprintf("%s", zName); zBase = strrchr(zDir, '/'); if( zBase==0 ){ zBase = zDir; zDir = ""; }else{ *zBase = 0; zBase++; } if( zName && zName[0] ){ /* this looks like navigation, but it's relative to the current page */ common_add_action_item(mprintf("dirview"), "Top"); common_add_action_item(mprintf("dirview?d=%T",zDir), "Up"); common_add_action_item(mprintf("dir?d=%T&sc=1",zName), "Short"); }else{ common_add_action_item("dir?sc=1", "Short"); } common_add_help_item("CvstracBrowse"); zNameNS = mprintf("%s",zName); if( zName[0] ){ zName = mprintf("%s/",zName); } /* Figure out how should we order this and display our intent in ** If no ordering preference is found, don't display anything in */ zDesc = P("desc") ? "DESC" : "ASC"; z = P("o"); if( z ){ zFld = z[0]; switch( zFld ){ case 'f': zOrderBy = mprintf("2 %s", zDesc); break; case 'v': zOrderBy = mprintf("3 %s", zDesc); break; case 'u': zOrderBy = mprintf("5 %s", zDesc); break; case 'd': zOrderBy = mprintf("6 %s", (strcmp(zDesc,"ASC")==0)?"DESC":"ASC"); break; case 'm': zOrderBy = mprintf("7 %s", zDesc); break; default: zFld = 0; break; } } db_add_functions(); az = db_query( "SELECT 0, f.base, fc.vers, fc.chngtype, c.user, c.date, " " '[' || f.lastcn || '] ' || c.message, f.lastcn " "FROM file f, chng c, filechng fc " "WHERE f.dir='%q' " " AND f.isdir=0 " " AND fc.filename=path(isdir,dir,base) " " AND f.lastcn=fc.cn " " AND f.lastcn=c.cn " "UNION ALL " "SELECT 1, f.base, NULL, NULL, NULL, c.date, " " NULL, f.lastcn " "FROM file f, chng c " "WHERE f.dir='%q' " " AND f.isdir=1 " " AND f.lastcn=c.cn " "ORDER BY %s", zNameNS, zNameNS, zOrderBy ); common_header("Directory /%h", zName); cgi_printf("\n" "\n"); column_header(zNameNS,zFld,"file","File"); column_header(zNameNS,zFld,"vers","Vers"); column_header(zNameNS,zFld,"user","By"); column_header(zNameNS,zFld,"date","Age"); column_header(zNameNS,zFld,"msg","Check-in"); cgi_printf("\n"); if( zName[0] ){ cgi_printf("\n",zDir); } /* In case dir is empty, exit nicely */ if( !az || !az[0] ){ cgi_printf("
\n" "\n",zDir); common_icon("backup"); cgi_printf(" ..
\n"); common_footer(); return; } for(i=0; az[i]; i+=8){ row_content(zName,i/8,&az[i]); } cgi_printf("\n"); db_query_free(az); common_footer(); } /* ** WEBPAGE: /fileview ** ** Show the file in a HTML page. In the case of things like images, show the ** content embedded in the page. */ void browse_fileview(void){ const char *zFile = g.zExtra ? g.zExtra : P("f"); const char *zVers = PD("v",""); char *zGetFile; char *zDir, *z; char *zSuffix; char *zMime = "text/plain"; /* The default MIME type */ /* The following table lists some alternative MIME types based on ** the file suffix */ static const struct { char *zSuffix; char *zMime; } aMime[] = { { "html", "text/html" }, { "htm", "text/html" }, { "gif", "image/gif" }, { "jpeg", "image/jpeg" }, { "jpg", "image/jpeg" }, { "png", "image/png" }, { "pdf", "application/pdf" }, { "ps", "application/postscript" }, { "eps", "application/postscript" }, }; login_check_credentials(); if( !g.okCheckout ){ login_needed(); return; } throttle(1,0); common_standard_menu(0, "search?f=1"); history_update(0); /* Make sure we always have '/' in zFile, otherwise link to parent ** directory won't work for file in repository root. */ if( strrchr(zFile, '/') ){ zDir = mprintf("%T?d=%T", default_browse_url(), zFile); }else{ zDir = mprintf("%T?d=/%T", default_browse_url(), zFile); } z = strrchr(zDir, '/' ); if( z ){ *z = 0;} common_add_nav_item(zDir, "Directory"); zGetFile = mprintf("getfile?f=%T&v=%T", zFile, zVers); common_add_action_item(zGetFile, "Raw"); add_file_tools(0,zFile,zVers,0); common_add_help_item("CvstracFileview"); common_header("%h %h", zFile, printable_vers(zVers)); /* sort out the MIME type. We output HTML, but some things are embeddable. */ zSuffix = strrchr(zFile, '.'); if( zSuffix ){ char zLine[2000]; int i; zSuffix++; for(i=0; zSuffix[i] && i%h\n" "%h
\n",zFile,zFile,zGetFile,zVers); /* For image types, embed in the page. Anything else, try to inline */ if( !strncmp(zMime,"image/",6) ){ cgi_printf("\"%h\n",zGetFile,zFile,zVers); }else{ if( dump_version(zVers,zFile,0) ){ cgi_redirect("index"); return; } } cgi_printf("
\n"); common_footer(); } /* ** WEBPAGE: /getfile ** ** Return the complete content of a file */ void browse_getfile(void){ const char *zFile = g.zExtra ? g.zExtra : P("f"); const char *zVers = P("v"); char *zSuffix; char *zMime = "text/plain"; /* The default MIME type */ /* The following table lists some alternative MIME types based on ** the file suffix */ static const struct { char *zSuffix; char *zMime; } aMime[] = { { "html", "text/html" }, { "htm", "text/html" }, { "gif", "image/gif" }, { "jpeg", "image/jpeg" }, { "jpg", "image/jpeg" }, { "png", "image/png" }, { "pdf", "application/pdf" }, { "ps", "application/postscript" }, { "eps", "application/postscript" }, }; login_check_credentials(); if( !g.okCheckout || zFile==0 ){ login_needed(); return; } throttle(1,0); if( zVers!= 0 ){ /* A database query is almost definitely going to be faster than having ** to pull from from the repository, so we might as well try this first. */ char *z = db_short_query("SELECT chng.date FROM filechng, chng " "WHERE filechng.filename='%q' " " AND filechng.vers='%q' " " AND filechng.cn=chng.cn ", zFile, zVers); if( z ){ cgi_modified_since(atoi(z)); cgi_append_header(mprintf("Last-Modified: %h\r\n", cgi_rfc822_datestamp(atoi(z)))); free(z); } } if( dump_version(zVers,zFile,1) ){ cgi_redirect("index"); return; } /* sort out the MIME type */ zSuffix = strrchr(zFile, '.'); if( zSuffix ){ char zLine[2000]; int i; zSuffix++; for(i=0; zSuffix[i] && i