cvstrac_2.0.1.orig/.vimrc0000644000000000000000000000020410666575630015475 0ustar00usergroup00000000000000" vi settings for CVSTrac tabstops " you need to ":set exrc" in ~/.exrc or ~/.vimrc :set expandtab :set tabstop=2 :set shiftwidth=2 cvstrac_2.0.1.orig/COMPILING0000644000000000000000000000234310666575630015626 0ustar00usergroup00000000000000How To Compile CVSTrac 1. You will need the SQLite library version 3.0.0 or newer. The 3.3.x series is recommended. If you do not already have this library installed, obtain a copy from http://www.sqlite.org 2. Choose a directory to compile in. The directory in which you compile does not need to be the same as the source directory. By default, the compilation directory should be a sibling of the source directory but this is not required. 3. Make a copy of one of the platform-specific *.mk files into the compilation directory and rename it 'Makefile'. Currently available are: bsd-gcc.mk linux-gcc.mk linux-mingw.mk osx-gcc.mk 4. Edit the file you just copied to adjust the parameters to your system. 5. Type "make" to build the CVSTrac executable. Type "make APPNAME=svntrac" to build a SvnTrac executable. Type "make APPNAME=gittrac" to build a GitTrac executable. 6. Copy the resulting executable to /usr/bin or /usr/local/bin or wherever you want to install it. For additional information on how CVSTrac is put together (information you may want to know if you would like to try to make improvements to CVSTrac) see the file called "howitworks.html". cvstrac_2.0.1.orig/COPYING0000644000000000000000000003543310666575630015423 0ustar00usergroup00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS cvstrac_2.0.1.orig/VERSION0000644000000000000000000000002410666575630015424 0ustar00usergroup00000000000000s/@VERSION@/2.0.1/g cvstrac_2.0.1.orig/attach.c0000644000000000000000000003110010666575630015763 0ustar00usergroup00000000000000/* ** 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/ ** ******************************************************************************* ** ** Code for handling attachments */ #include "config.h" #include "attach.h" #include /* ** The default maximum size of an attachment. This value can be changed ** by the setup users. */ #define MX_ATTACH_SIZE "102400" /* ** Return the maximum size of an attachment in bytes */ int attachment_max(void){ return atoi(db_config("max_attach_size",MX_ATTACH_SIZE)); } int is_integer(const char *zString){ if( zString==0 ) return 0; while( *zString ){ if( !isdigit(*zString) ) return 0; zString ++; } return 1; } /* ** WEBPAGE: /attach_add ** ** This web-page gives the user the opportunity to add an attachment to ** an existing ticket. The "tn" query parameter specifies the ticket ** number. A tn of zero (an invalid ticket number) means attachments to ** the setup page (i.e. stylesheets, logos, etc). ** ** This routine has been extended so that "tn" can now be the name of ** a Wiki page. This allows attachments to be added to wiki. */ void attachment_add(void){ const char *zPage; char *zTitle = NULL; char *zErr = 0; const char *zBack; int mxSize = attachment_max(); int tn = atoi(PD("tn","0")); zPage = P("tn"); if( zPage==0 ){ common_err("Invalid or missing \"tn\" query parameter"); } login_check_credentials(); throttle(1,1); if( is_integer(zPage) ){ if( tn ){ zBack = mprintf("tktview?tn=%d", tn); if( !g.okWrite ){ cgi_redirect(zBack); } }else{ zBack = "setup_style"; if( !g.okSetup ){ cgi_redirect("index"); } } }else{ zBack = mprintf("wiki?p=%t", zPage); if( is_user_page(zPage) ){ /* only admins and the user ccan attach to the users home page */ if( !g.okAdmin && !is_home_page(zPage) ){ cgi_redirect(zBack); } }else if( is_wiki_name(zPage)!=strlen(zPage) ){ common_err("Invalid wiki page name \"tn=%h\"", zPage); }else if( !g.okWiki ){ cgi_redirect(zBack); } } common_add_help_item("CvstracAttachment"); common_add_action_item(zBack, "Cancel"); if( P("can") || mxSize<=0 ){ cgi_redirect(zBack); } if( P("ok") ){ const char *zData = P("f"); const char *zDescription = PD("d",""); const char *zName = P("f:filename"); int size = atoi(PD("f:bytes","0")); const char *zType = PD("f:mimetype","text/plain"); const char *z; time_t now = time(0); char **az; int atn; if( zData==0 || zName==0 || zName[0]==0 || size<=0 || zType==0 ){ common_err("Attachment information is missing from the query content"); } if( size>mxSize ){ zErr = mprintf("Your attachment is too big. The maximum allowed size " "is %dKB but your attachment was %dKB", mxSize/1024, (size+1023)/1024); }else{ sqlite3 *pDb; sqlite3_stmt *pStmt; const char *zTail; int rc; for(z=zName; *z; z++){ if( (*z=='/' || *z=='\\') && z[1]!=0 ){ zName = &z[1]; } } /* ** In order to insert a blob, we need to drop down to raw SQLite 3 ** calls. */ pDb = db_open(); rc = sqlite3_prepare( pDb, "INSERT INTO " " attachment(atn,tn,size,date,user,description,mime,fname,content) " "VALUES(NULL,?,?,?,?,?,?,?,?);", -1, &pStmt, &zTail); if( rc!=SQLITE_OK ) { db_err( sqlite3_errmsg(pDb), 0, "/attach_add: unable to add \"%h\"", zName ); } sqlite3_bind_text(pStmt, 1, zPage, -1, SQLITE_STATIC); sqlite3_bind_int(pStmt, 2, size); sqlite3_bind_int(pStmt, 3, now); sqlite3_bind_text(pStmt, 4, g.zUser, -1, SQLITE_STATIC); sqlite3_bind_text(pStmt, 5, zDescription, -1, SQLITE_STATIC); sqlite3_bind_text(pStmt, 6, zType, -1, SQLITE_STATIC); sqlite3_bind_text(pStmt, 7, zName, -1, SQLITE_STATIC); sqlite3_bind_blob(pStmt, 8, zData, size, SQLITE_STATIC); rc = sqlite3_step(pStmt); if( rc!=SQLITE_DONE ) { db_err( sqlite3_errmsg(pDb), 0, "/attach_add: unable to add \"%h\"", zName ); } sqlite3_finalize(pStmt); az = db_query( "SELECT MAX(ROWID) FROM attachment" ); atn = atoi(az[0]); if( tn ) ticket_notify(atoi(zPage), 0, 0, atn); cgi_redirect(zBack); } } if( is_integer(zPage) ){ if( tn==0 ){ /* FIXME: Not sure we need a separate page unless there's an error... */ common_header("Attachments To Setup"); }else{ zTitle = db_short_query("SELECT title FROM ticket WHERE tn=%d", tn); if( zTitle==0 ){ common_err("No such ticket: #%d", tn); } common_header("Attachments To Ticket #%d", tn); } }else{ common_header("Attachments to %h", wiki_expand_name(zPage)); } if( zErr ){ @

Error: %h(zErr)

} if( is_integer(zPage) && tn ){ @

Ticket #%d(tn): %h(zTitle)

} if( attachment_html(zPage, "

Existing attachments:

", "")==0 ){ @

There are currently no attachments on this document.

} @

To add a new attachment @ select the file to attach below an press the "Add Attachment" button. @ Attachments may not be larger than %d(mxSize/1024)KB.

@
@ File to attach:
@ Description: @ (See formatting hints)
@
@ @ @ @
@
@ @

Formatting Hints:

append_formatting_hints(); common_footer(); } static int output_attachment_callback( void *nGot_, /* Set if we got results */ int nArg, /* Number of columns in this result row */ char **azArg, /* Text of data in all columns */ char **azName /* Names of the columns */ ){ int *nGot = (int *)nGot_; if( nArg != 5 ) return 0; (*nGot) ++; cgi_set_content_type(azArg[1]); cgi_modified_since(atoi(azArg[3])); cgi_append_header( mprintf("Last-Modified: %s\r\n",cgi_rfc822_datestamp(atoi(azArg[3])))); cgi_append_header(mprintf("Content-disposition: attachment; " "filename=\"%T\"\r\n", azArg[4])); cgi_append_content(azArg[2], atoi(azArg[0])); g.isConst = 1; return 0; } void attachment_output(int atn){ /* ** We need to use a callback here since the content is a BLOB type object ** and the usual db_query() won't handle NUL characters in a returned ** row. The callback has the full row buffer available and will handle ** all the output duties. got will be set if we get a row. */ int got = 0; db_callback_query( output_attachment_callback, &got, "SELECT size, mime, content, date, fname " "FROM attachment " "WHERE atn=%d", atn); if( !got ){ common_err("No such attachment: %d", atn); } } /* ** WEBPAGE: /attach_get ** ** Retrieve an attachment. g.zExtra looks something like "90/file.gif", which ** the atoi() call turns into just the integer "90". The filename is ignored, ** although some browsers use it as an initial name when saving to disk. */ void attachment_get(void){ int atn = g.zExtra ? atoi(g.zExtra) : 0; char *z; login_check_credentials(); throttle(1,0); if( atn==0 ) common_err("No attachment specified"); z = db_short_query("SELECT tn FROM attachment WHERE atn=%d", atn); if( z && z[0] ){ if( is_integer(z) ){ if( !g.okRead ){ login_needed(); return; } }else{ if( !g.okRdWiki ){ login_needed(); return; } } attachment_output(atn); }else{ common_err("No attachment specified"); } } /* ** Return true if it is ok to delete an attachment created by zUser ** at time addTime. Rules: ** ** * The Setup user can delete any attachment no matter who added ** it or how old it is. ** ** * Any registered user can delete an attachment that they ** themselves created less than 24 hours ago. ** ** * Users with Delete privilege can delete an attachment added ** by anonymous within the past 24 hours. ** */ int ok_to_delete_attachment(int addTime, const char *zUser){ if( g.okSetup ){ return 1; } if( addTimeAre you sure you want to delete this attachments?

@
@ %h(az[5]) %h(az[1]) bytes added by %h(az[3]) on %h(zDate) UTC. if(az[6] && az[6][0]){ @
output_formatted(az[6], NULL); @
} @
@ @
@ @      @ @      @ @
common_footer(); } /* ** This routine generates HTML that shows a list of attachments for ** the given ticket number or wiki page. If there are no attachments, ** nothing is generated. Return the number of attachments. */ int attachment_html(const char *zPage, const char *zBefore, const char *zAfter){ char **az; int i = 0; time_t now; if( is_integer(zPage) ){ if( !g.okRead ) return 0; }else{ if( !g.okRdWiki ) return 0; } az = db_query("SELECT atn, size, date, user, mime, fname, description " "FROM attachment WHERE tn='%q' ORDER BY date", zPage); time(&now); if( az[0] ){ @ %s(zBefore) @
@ %s(zAfter) } db_query_free(az); return i/7; } cvstrac_2.0.1.orig/browse.c0000644000000000000000000006071510666575630016036 0ustar00usergroup00000000000000/* ** 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); @ 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 ){ @ @ @ } if( i%2 ){ @ }else{ @ } @ if( az[i][0]==0 ){ @ if( az[i+8] && az[i+8][0] ){ @ if( az[i+8] && az[i+8][0] ){ @ } if( i==0 ){ @ } @
DateVersionDescription
%s(zDate) common_icon("box"); @ @ Milestone output_chng(atoi(az[i+2])); @ on branch %h(az[i+8]): }else{ @ @ Milestone output_chng(atoi(az[i+2])); } }else{ @    @ @ %h(printable_vers(az[i+2])) @   Check-in output_chng(atoi(az[i])); @ on branch %h(az[i+8]): }else{ @ Check-in output_chng(atoi(az[i])); @ : } } output_formatted(az[i+6], 0); @ By %z(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] ){ @ @ (diff) } } @
Nothing is known about this file
} /* ** 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); @ %h(zFile) if( zVers1 ){ char *zFV = mprintf("fileview?f=%T&v=%T", zFile, zVers1); @ %h(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) ){ @ Diff failed! } 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)

*/ @ @ for(i=j=0; i<4; i++){ @ } @
n = 0; if( i==0 && zName[0] ){ @ common_icon("backup"); @  ..
n++; } while( n common_icon("dir"); @  %h(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); @ common_icon(zIcon); @  %h(az[j])
} n++; j += 2; } @
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" ) : ""; /* Clicking same column header 3 times in a row resets any sorting. */ if(set && desc){ @ @ %h(zColumn) return; } if(set){ @ %h(zColumn) } /* ** Output a long directory row */ static void row_content( const char *zName, const char *zSortUrl, int nCol, char **az ){ if( (nCol%2)==0 ){ @ }else{ @ } if( atoi(az[0])==1 ){ @ @ common_icon("dir"); @  %h(az[1])/ @ %h(file_age_to_text(atoi(az[5]))) @ }else{ @ @ if( atoi(az[3])==2 ){ common_icon("del"); }else{ common_icon("file"); } @  %h(az[1]) @ @ @ %h(printable_vers(az[2])) @ %z(format_user(az[4])) @ %h(file_age_to_text( atoi(az[5]) )) @ if( output_trim_message(az[6], MN_CKIN_MSG, MX_CKIN_MSG) ){ output_formatted(az[6], 0); @  [...] }else{ output_formatted(az[6], 0); } @ } @ } /* ** 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; char *zSortUrl = ""; 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++; } /* 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 ){ zSortUrl = mprintf("o=%t%s", z, (zDesc[0]=='D')?"&desc":""); 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; } } if( zName && zName[0] ){ /* this looks like navigation, but it's relative to the current page */ common_add_action_item( mprintf("dirview%s%s",(zSortUrl[0])?"?":"",zSortUrl), "Top"); common_add_action_item( mprintf("dirview?d=%T%s%s",zDir,(zSortUrl[0])?"&":"",zSortUrl), "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); } 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); @ @ 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"); @ if( zName[0] ){ @ } /* In case dir is empty, exit nicely */ if( !az || !az[0] ){ @
@ common_icon("backup"); @  ..
common_footer(); return; } for(i=0; az[i]; i+=8){ row_content(zName,zSortUrl,i/8,&az[i]); } @ 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(zFile) @ %h(zVers)
/* For image types, embed in the page. Anything else, try to inline */ if( !strncmp(zMime,"image/",6) ){ @ %h(zFile) %h(zVers) }else{ if( dump_version(zVers,zFile,0) ){ cgi_redirect("index"); return; } } @
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; const char *zName; 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 #include #include #include #include #include #include #include #include #include #include #include #include #include "cgi.h" #if INTERFACE /* ** Shortcuts for cgi_parameter. P("x") returns the value of query parameter ** or cookie "x", or NULL if there is no such parameter or cookie. PD("x","y") ** does the same except "y" is returned in place of NULL if there is not match. */ #define P(x) cgi_parameter((x),0) #define PD(x,y) cgi_parameter((x),(y)) #define QP(x) quotable_string(cgi_parameter((x),0)) #define QPD(x,y) quotable_string(cgi_parameter((x),(y))) #endif /* INTERFACE */ /* ** Provide a reliable implementation of a caseless string comparison ** function. */ #define stricmp sqlite3StrICmp extern int sqlite3StrICmp(const char*, const char*); /* ** The body of the HTTP reply text is stored here. */ static int nAllocTxt = 0; /* Amount of space allocated for HTTP reply text */ static int nUsedTxt = 0; /* Amount of space actually used */ static char *zTxt = 0; /* Pointer to malloced space */ /* ** Append reply content to what already exists. Return a pointer ** to the start of the appended text. */ const char *cgi_append_content(const char *zData, int nAmt){ if( nUsedTxt+nAmt >= nAllocTxt ){ nAllocTxt = nUsedTxt*2 + nAmt + 1000; zTxt = realloc( zTxt, nAllocTxt ); if( zTxt==0 ) exit(1); } memcpy(&zTxt[nUsedTxt], zData, nAmt); nUsedTxt += nAmt; return &zTxt[nUsedTxt-nAmt]; } /* ** Reset the HTTP reply text to be an empty string. */ void cgi_reset_content(void){ nUsedTxt = 0; g.zLinkURL = 0; } /* ** Return a pointer to the HTTP reply text. The text is reset */ char *cgi_extract_content(int *pnAmt){ char *z; *pnAmt = nUsedTxt; if( zTxt ){ zTxt[nUsedTxt] = 0; } z = zTxt; zTxt = 0; nAllocTxt = 0; nUsedTxt = 0; return z; } /* ** Additional information used to form the HTTP reply */ static char *zContentType = "text/html"; /* Content type of the reply */ static char *zReplyStatus = "OK"; /* Reply status description */ static int iReplyStatus = 200; /* Reply status code */ static char *zExtraHeader = 0; /* Extra header text */ static int fullHttpReply = 0; /* True for a full-blown HTTP header */ static const char *zLogFile = 0; /* Name of the log file */ static const char *zLogArg = 0; /* Argument to the log message */ /* ** Set the reply content type */ void cgi_set_content_type(const char *zType){ zContentType = mprintf("%s", zType); } /* ** Set the reply status code */ void cgi_set_status(int iStat, const char *zStat){ zReplyStatus = mprintf("%s", zStat); iReplyStatus = iStat; } /* ** Append text to the header of an HTTP reply */ void cgi_append_header(const char *zLine){ if( zExtraHeader ){ zExtraHeader = mprintf("%z%s", zExtraHeader, zLine); }else{ zExtraHeader = mprintf("%s", zLine); } } /* ** Set a cookie. ** ** Zero lifetime implies a session cookie. */ void cgi_set_cookie( const char *zName, /* Name of the cookie */ const char *zValue, /* Value of the cookie. Automatically escaped */ const char *zPath, /* Path cookie applies to. NULL means "/" */ int lifetime /* Expiration of the cookie in seconds */ ){ char *zCookie; if( zPath==0 ) zPath = "/"; if( lifetime>0 ){ lifetime += (int)time(0); zCookie = mprintf("Set-Cookie: %s=%t; Path=%s; expires=%s; Version=1\r\n", zName, zValue, zPath, cgi_rfc822_datestamp(lifetime)); }else{ zCookie = mprintf("Set-Cookie: %s=%t; Path=%s; Version=1\r\n", zName, zValue, zPath); } cgi_append_header(zCookie); free(zCookie); } /* ** This routine sets up the name of a logfile and an argument to the ** log message. The log message is written when cgi_reply() is invoked. */ void cgi_logfile(const char *zFile, const char *zArg){ if( zFile ) zLogFile = zFile; zLogArg = zArg; } static char *cgi_add_etag(char *zTxt, int nLen){ MD5Context ctx; unsigned char digest[16]; int i, j; char zETag[64]; MD5Init(&ctx); MD5Update(&ctx,(unsigned char *)zTxt,(unsigned int)nLen); MD5Final(digest,&ctx); for(j=i=0; i<16; i++,j+=2){ bprintf(&zETag[j],sizeof(zETag)-j,"%02x",(int)digest[i]); } cgi_append_header( mprintf("ETag: %s\r\n", zETag) ); return strdup(zETag); } /* ** Do some cache control stuff. First, we generate an ETag and include it in ** the response headers. Second, we do whatever is necessary to determine if ** the request was asking about caching and whether we need to send back the ** response body. If we shouldn't send a body, return non-zero. ** ** Currently, we just check the ETag against any If-None-Match header. ** ** FIXME: In some cases (attachments, file contents) we could check ** If-Modified-Since headers and always include Last-Modified in responses. */ static int check_cache_control(void){ /* FIXME: there's some gotchas wth cookies and some headers. */ char *zETag = cgi_add_etag(zTxt,nUsedTxt); char *zMatch = getenv("HTTP_IF_NONE_MATCH"); if( zETag!=0 && zMatch!=0 ) { char *zBuf = strdup(zMatch); if( zBuf!=0 ){ char *zTok = 0; char *zPos; for( zTok = strtok_r(zBuf, ",\"",&zPos); zTok && strcasecmp(zTok,zETag); zTok = strtok_r(0, ",\"",&zPos)){} free(zBuf); if(zTok) return 1; } } return 0; } /* ** Do a normal HTTP reply */ void cgi_reply(void){ FILE *log; if( iReplyStatus<=0 ){ iReplyStatus = 200; zReplyStatus = "OK"; } if( iReplyStatus==200 && check_cache_control() ) { /* change the status to "unchanged" and we can skip sending the ** actual response body. Obviously we only do this when we _have_ a ** body (code 200). */ iReplyStatus = 304; zReplyStatus = "Not Modified"; } if( fullHttpReply ){ printf("HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); printf("Date: %s\r\n", cgi_rfc822_datestamp(time(0))); printf("Connection: close\r\n"); }else{ printf("Status: %d %s\r\n", iReplyStatus, zReplyStatus); } if( zExtraHeader ){ printf("%s", zExtraHeader); } if( g.isConst ){ /* constant means that the input URL will _never_ generate anything ** else. In the case of attachments, the contents won't change because ** an attempt to change them generates a new attachment number. In the ** case of most /getfile calls for specific versions, the only way the ** content changes is if someone breaks the SCM. And if that happens, a ** stale cache is the least of the problem. So we provide an Expires ** header set to a reasonable period (default: one week). */ time_t expires = time(0) + atoi(db_config("constant_expires","604800")); printf( "Expires: %s\r\n", cgi_rfc822_datestamp(expires)); } if( g.isAnon ){ printf("Cache-control: public\r\n"); }else{ /* Content intended for logged in users should only be cached in ** the browser, not some shared location. */ printf("Cache-control: private\r\n"); } #if CVSTRAC_I18N printf( "Content-Type: %s; charset=%s\r\n", zContentType, nl_langinfo(CODESET)); #else printf( "Content-Type: %s; charset=ISO-8859-1\r\n", zContentType); #endif if( iReplyStatus != 304 ) { printf( "Content-Length: %d\r\n", nUsedTxt ); } printf("\r\n"); if( zTxt && iReplyStatus != 304 ){ fwrite(zTxt, 1, nUsedTxt, stdout); } if( zLogFile && (log = fopen(zLogFile,"a"))!=0 ){ time_t now; struct tm *pTm; const char *zPath; const char *zAddr; struct tms sTms; double rScale; char zDate[200]; if( zLogArg==0 ) zLogArg = "*"; zPath = getenv("REQUEST_URI"); if( zPath==0 ) zPath = "/"; zAddr = getenv("REMOTE_ADDR"); if( zAddr==0 ) zAddr = "*"; time(&now); pTm = localtime(&now); strftime(zDate, sizeof(zDate), "%Y-%m-%d %H:%M:%S", pTm); fprintf(log, "%s %s %s %d %s", zDate, zAddr, zPath, iReplyStatus,zLogArg); times(&sTms); rScale = 1.0/(double)sysconf(_SC_CLK_TCK); fprintf(log, " %g %g %g %g\n", rScale*sTms.tms_utime, rScale*sTms.tms_stime, rScale*sTms.tms_cutime, rScale*sTms.tms_cstime); fclose(log); } } static int is_same_page(const char *zPage1, const char *zPage2){ size_t s1 = strcspn(zPage1,"?#"); size_t s2 = strcspn(zPage2,"?#"); if( s1 != s2 ) return 0; return !strncmp(zPage1,zPage2,s1); } /* ** Do a redirect request to the URL given in the argument. ** ** The URL might be relative to the current document. If so, ** this routine has to translate the URL into an absolute ** before formatting the reply. */ void cgi_redirect(const char *zURL){ char *zLocation; if( strncmp(zURL,"http:",5)==0 || strncmp(zURL,"https:",6)==0 || *zURL=='/' ){ /* An absolute URL. Do nothing */ }else{ int i, j, k=0; char *zDest; char *zCur = getenv("REQUEST_URI"); if( zCur==0 ) zCur = ""; for(i=0; zCur[i] && zCur[i]!='?' && zCur[i]!='#'; i++){} if( g.zExtra ){ /* Skip to start of extra stuff, then pass over any /'s that might ** have separated the document root from the extra stuff. This ** ensures that the redirection actually redirects the root, not ** something deep down at the bottom of a URL. */ i -= strlen(g.zExtra); while( i>0 && zCur[i-1]=='/' ){ i--; } } while( i>0 && zCur[i-1]!='/' ){ i--; } zDest = mprintf("%.*s/%s", i, zCur, zURL); /* don't touch the protocol stuff, if it exists */ if( !strncmp(zDest,"http://",7) ){ k = 7; }else if( !strncmp(zDest,"https://",8) ){ k = 8; } /* strip out constructs like .., /./, //, etc */ for(i=j=k; zDest[i]; i++){ if( zDest[i]=='/' ){ if( zDest[i+1]=='.' ){ if( zDest[i+2]=='/' ){ i += 2; continue; } if( zDest[i+2]=='.' && zDest[i+3]=='/' ){ if( j==0 ){ i += 3; continue; } j--; while( j>0 && zDest[j-1]!='/' ){ j--; } continue; } } if( zDest[i+1]=='/' && (i==0 || zDest[i-1]!=':') ) continue; } zDest[j++] = zDest[i]; } zDest[j] = 0; zURL = zDest; /* see if we've got a cycle by matching everything up to the ? or # ** in the new and old URLs. */ if( is_same_page(zDest,zCur) ){ cgi_reset_content(); cgi_printf("\n

Cyclic redirection in %s

\n\n", zURL); cgi_set_status(500, "Internal Server Error"); cgi_reply(); exit(0); } } /* ** The lynx browser complains if the "http:" prefix is missing ** from a redirect. But if we use it, we lose the ability to ** run on a secure server using stunnel. ** ** Relative redirects are used by default. This works with stunnel. ** Lynx sends us a nasty message, but it still works. So with ** relative redirects everybody works. With absolute redirects, ** stunnel will not work. So the default configuration is to go ** with what works for everybody, even if it happens to be technically ** incorrect. */ #ifdef ABSOLUTE_REDIRECT { char *zHost; if( strncmp(zURL,"http:",5)!=0 && strncmp(zURL,"https:",6)!=0 && (zHost = getenv("HTTP_HOST"))!=0 ){ char *zMode = getenv("HTTPS"); if( zMode && strcmp(zMode,"on")==0 ){ zURL = mprintf("https://%s%s", zHost, zURL); }else{ zURL = mprintf("http://%s%s", zHost, zURL); } } } #endif zLocation = mprintf("Location: %s\r\n", zURL); cgi_append_header(zLocation); cgi_reset_content(); cgi_printf("\n

Redirect to %h

\n\n", zURL); cgi_set_status(302, "Moved Temporarily"); free(zLocation); cgi_reply(); exit(0); } /* ** Information about all query parameters and cookies are stored ** in these variables. */ static int nAllocQP = 0; /* Space allocated for aParamQP[] */ static int nUsedQP = 0; /* Space actually used in aParamQP[] */ static int sortQP = 0; /* True if aParamQP[] needs sorting */ static struct QParam { /* One entry for each query parameter or cookie */ char *zName; /* Parameter or cookie name */ char *zValue; /* Value of the query parameter or cookie */ } *aParamQP; /* An array of all parameters and cookies */ /* ** Add another query parameter or cookie to the parameter set. ** zName is the name of the query parameter or cookie and zValue ** is its fully decoded value. ** ** zName and zValue are not copied and must not change or be ** deallocated after this routine returns. */ static void cgi_set_parameter_nocopy(char *zName, char *zValue){ if( nAllocQP<=nUsedQP ){ nAllocQP = nAllocQP*2 + 10; aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) ); if( aParamQP==0 ) exit(1); } aParamQP[nUsedQP].zName = zName; aParamQP[nUsedQP].zValue = zValue; nUsedQP++; sortQP = 1; } /* ** Add another query parameter or cookie to the parameter set. ** zName is the name of the query parameter or cookie and zValue ** is its fully decoded value. ** ** Copies are made of both the zName and zValue parameters. */ void cgi_set_parameter(const char *zName, const char *zValue){ cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue)); } /* ** Add a list of query parameters or cookies to the parameter set. ** ** Each parameter is of the form NAME=VALUE. Both the NAME and the ** VALUE may be url-encoded ("+" for space, "%HH" for other special ** characters). But this routine assumes that NAME contains no ** special character and therefore does not decode it. ** ** Parameters are separated by the "terminator" character. Whitespace ** before the NAME is ignored. ** ** The input string "z" is modified but no copies is made. "z" ** should not be deallocated or changed again after this routine ** returns or it will corrupt the parameter table. */ static void add_param_list(char *z, int terminator){ while( *z ){ char *zName; char *zValue; while( isspace(*z) ){ z++; } zName = z; while( *z && *z!='=' && *z!=terminator ){ z++; } if( *z=='=' ){ *z = 0; z++; zValue = z; while( *z && *z!=terminator ){ z++; } if( *z ){ *z = 0; z++; } dehttpize(zValue); cgi_set_parameter_nocopy(zName, zValue); }else{ if( *z ){ *z++ = 0; } cgi_set_parameter_nocopy(zName, ""); } } } /* ** *pz is a string that consists of multiple lines of text. This ** routine finds the end of the current line of text and converts ** the "\n" or "\r\n" that ends that line into a "\000". It then ** advances *pz to the beginning of the next line and returns the ** previous value of *pz (which is the start of the current line.) */ static char *get_line_from_string(char **pz, int *pLen){ char *z = *pz; int i; if( z[0]==0 ) return 0; for(i=0; z[i]; i++){ if( z[i]=='\n' ){ if( i>0 && z[i-1]=='\r' ){ z[i-1] = 0; }else{ z[i] = 0; } i++; break; } } *pz = &z[i]; *pLen -= i; return z; } /* ** The input *pz points to content that is terminated by a "\r\n" ** followed by the boundry marker zBoundry. An extra "--" may or ** may not be appended to the boundry marker. There are *pLen characters ** in *pz. ** ** This routine adds a "\000" to the end of the content (overwriting ** the "\r\n" and returns a pointer to the content. The *pz input ** is adjusted to point to the first line following the boundry. ** The length of the content is stored in *pnContent. */ static char *get_bounded_content( char **pz, /* Content taken from here */ int *pLen, /* Number of bytes of data in (*pz)[] */ char *zBoundry, /* Boundry text marking the end of content */ int *pnContent /* Write the size of the content here */ ){ char *z = *pz; int len = *pLen; int i; int nBoundry = strlen(zBoundry); *pnContent = len; for(i=0; i0 && z[i-1]=='\r' ) i--; z[i] = 0; *pnContent = i; i += nBoundry; break; } } *pz = &z[i]; get_line_from_string(pz, pLen); return z; } /* ** Tokenize a line of text into as many as nArg tokens. Make ** azArg[] point to the start of each token. ** ** Tokens consist of space or semi-colon delimited words or ** strings inside double-quotes. Example: ** ** content-disposition: form-data; name="fn"; filename="index.html" ** ** The line above is tokenized as follows: ** ** azArg[0] = "content-disposition:" ** azArg[1] = "form-data" ** azArg[2] = "name=" ** azArg[3] = "fn" ** azArg[4] = "filename=" ** azArg[5] = "index.html" ** azArg[6] = 0; ** ** '\000' characters are inserted in z[] at the end of each token. ** This routine returns the total number of tokens on the line, 6 ** in the example above. */ static int tokenize_line(char *z, int mxArg, char **azArg){ int i = 0; while( *z ){ while( isspace(*z) || *z==';' ){ z++; } if( *z=='"' && z[1] ){ *z = 0; z++; if( i0 && zType && (strcmp(zType,"application/x-www-form-urlencoded")==0 || strncmp(zType,"multipart/form-data",19)==0) ){ z = malloc( len+1 ); if( z==0 ) exit(1); len = fread(z, 1, len, stdin); z[len] = 0; if( zType[0]=='a' ){ add_param_list(z, '&'); }else{ process_multipart_form_data(z, len); } } z = getenv("HTTP_COOKIE"); if( z ){ z = mprintf("%s",z); add_param_list(z, ';'); } } /* ** This is the comparison function used to sort the aParamQP[] array of ** query parameters and cookies. */ static int qparam_compare(const void *a, const void *b){ struct QParam *pA = (struct QParam*)a; struct QParam *pB = (struct QParam*)b; return strcmp(pA->zName, pB->zName); } /* ** Return the value of a query parameter or cookie whose name is zName. ** If there is no query parameter or cookie named zName, then return ** zDefault instead. */ const char *cgi_parameter(const char *zName, const char *zDefault){ int lo, hi, mid, c; if( nUsedQP<=0 ) return zDefault; if( sortQP ){ qsort(aParamQP, nUsedQP, sizeof(aParamQP[0]), qparam_compare); sortQP = 0; } lo = 0; hi = nUsedQP-1; while( lo<=hi ){ mid = (lo+hi)/2; c = strcmp(aParamQP[mid].zName, zName); if( c==0 ){ return aParamQP[mid].zValue; }else if( c>0 ){ hi = mid-1; }else{ lo = mid+1; } } return zDefault; } /* ** Return true if any of the query parameters in the argument ** list are defined. */ int cgi_any(const char *z, ...){ va_list ap; char *z2; if( cgi_parameter(z,0)!=0 ) return 1; va_start(ap, z); while( (z2 = va_arg(ap, char*))!=0 ){ if( cgi_parameter(z2,0)!=0 ) return 1; } va_end(ap); return 0; } /* ** Return true if all of the query parameters in the argument list ** are defined. */ int cgi_all(const char *z, ...){ va_list ap; char *z2; if( cgi_parameter(z,0)==0 ) return 0; va_start(ap, z); while( (z2 = va_arg(ap, char*))==0 ){ if( cgi_parameter(z2,0)==0 ) return 0; } va_end(ap); return 1; } /* ** Print all query parameters on standard output. Format the ** parameters as HTML. This is used for testing and debugging. */ void cgi_print_all(void){ int i; cgi_parameter("",""); /* For the parameters into sorted order */ for(i=0; i\n", htmlize(aParamQP[i].zName, -1), htmlize(aParamQP[i].zValue, -1)); } } /* ** Write HTML text for an option menu to standard output. zParam ** is the query parameter that the option menu sets. zDflt is the ** initial value of the option menu. Additional arguments are name/value ** pairs that define values on the menu. The list is terminated with ** a single NULL argument. */ void cgi_optionmenu(int in, const char *zP, const char *zD, ...){ va_list ap; char *zName, *zVal; int dfltSeen = 0; cgi_printf("%*s\n", in, ""); } /* ** This routine works a lot like cgi_optionmenu() except that the list of ** values is contained in an array. Also, the values are just values, not ** name/value pairs as in cgi_optionmenu. */ void cgi_v_optionmenu( int in, /* Indent by this amount */ const char *zP, /* The query parameter name */ const char *zD, /* Default value */ const char **az /* NULL-terminated list of allowed values */ ){ const char *zVal; int i; cgi_printf("%*s\n", in, ""); } /* ** This routine works a lot like cgi_v_optionmenu() except that the list ** is a list of pairs. The first element of each pair is the value used ** internally and the second element is the value displayed to the user. */ void cgi_v_optionmenu2( int in, /* Indent by this amount */ const char *zP, /* The query parameter name */ const char *zD, /* Default value */ const char **az /* NULL-terminated list of allowed values */ ){ const char *zVal; int i; cgi_printf("%*s\n", in, ""); } /* ** This routine should never be called directly. Use wrapper functions below. ** Generates HTML input element to be used in forms. ** Parameters are explained below inline. If any param is 0 that ** attribute/feature will not be used. zValue is required, except for text ** fields. zName is also required except for submit, reset and button. */ void cgi_input_elem( int nType, /* 1:submit, 2:reset, 3:button, 4:file, 5:hidden, ** 6:checkbox, 7:radio, 8:password, 9:text */ const char *zName, /* CGI param name */ const char *zId, /* HTML element id */ const char *zClass, /* CSS class to apply */ char nAccessKey, /* Access key to assign */ int nTabIndex, /* Element's tab index */ int nSize, /* Used only for text fields */ int nMaxLen, /* Used only for text fields */ int nLabelOnLeft, /* If set, put label text left of element */ const char *zValue, /* Element's value */ const char *zDflt, /* If same as zValue, "select" this element */ const char *zLabel /* Label text. No HTML escaping is done on it */ ){ /* Buttons and hidden fields can't have label */ int bHasLabel = ( nType>4 && zLabel && zLabel[0] ); assert( nType > 0 ); assert( nType <= 9 ); if( zValue==0 ) return; if( nType<1 && nType>3 && (!zName || !zName[0]) ) return; if( bHasLabel ){ /* Make sure we have some valid id because