cvstrac/wiki.c
1.39
/*
** 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 generated the Wiki pages
*/
#include "config.h"
#include "wiki.h"
#include <time.h>
/*
** Expand a wiki page name by adding a single space before each
** capital letter after the first. The returned string is written
** into space obtained from malloc().
*/
char *wiki_expand_name(const char *z){
int i, n;
char *zOut;
for(n=i=0; z[i]; i++, n++){
if( isupper(z[i]) ) n++;
}
zOut = malloc(n+1);
if( zOut==0 ) return "<out of memory>";
for(n=i=0; z[i]; i++, n++){
if( n>0 && isupper(z[i]) ){ zOut[n++] = ' '; }
zOut[n] = z[i];
}
zOut[n] = 0;
return zOut;
}
/*
** Write a string in zText into a temporary file. Write the name of
** the temporary file in zFile. Return 0 on success and 1 if there is
** any kind of error.
*/
int write_to_temp(const char *zText, char *zFile){
extern int sqliteOsTempFileName(char*);
FILE *f;
if( sqliteOsTempFileName(zFile) ){ zFile[0] = 0; return 1; }
f = fopen(zFile, "w");
if( f==0 ){ zFile[0] = 0; return 1; }
fwrite(zText, 1, strlen(zText), f);
fprintf(f, "\n");
fclose(f);
return 0;
}
/*
** Output a <PRE> formatted diff of two strings.
*/
void diff_strings(int nContext,const char *zString1, const char *zString2){
char zF1[200], zF2[200];
zF1[0] = zF2[0] = 0;
if( !write_to_temp(zString1, zF1) && !write_to_temp(zString2, zF2) ){
char *zCmd;
FILE *p;
char zLine[2000];
int cnt = 0;
zCmd = mprintf("diff -C %d -b '%s' '%s'", nContext,
quotable_string(zF1), quotable_string(zF2));
if( zCmd ){
p = popen(zCmd, "r");
if( p ){
@ <pre>
while( fgets(zLine, sizeof(zLine), p) ){
cnt++;
if( cnt>3 ) cgi_printf("%h", zLine);
}
@ </pre>
pclose(p);
}else{
common_err("Unable to diff temporary files");
}
free(zCmd);
}
} else {
common_err("Unable to create a temporary file");
}
if( zF1[0] ) unlink(zF1);
if( zF2[0] ) unlink(zF2);
}
/*
** WEBPAGE: /wiki.txt
**
** View a text version of a wiki page.
**
** Query parameters are "p" and "t". "p" is the name of the page to
** view. If "p" is omitted, the "WikiIndex" page is shown. "t" is
** the time (seconds since 1970) that determines which page to view.
** If omitted, the current time is substituted for "t".
*/
void wiki_text_page(void){
const char *pg = P("p");
const char *zTime = P("t");
int tm;
char **azPage;
login_check_credentials();
if( !g.okRdWiki ){ login_needed(); return; }
throttle(0,0);
if( zTime==0 || (tm = atoi(zTime))==0 ){
time_t now;
time(&now);
tm = now;
}
if( pg==0 || is_wiki_name(pg)!=strlen(pg) ){
pg = "WikiIndex";
}
azPage = db_query(
"SELECT text,invtime FROM wiki WHERE name='%q' AND invtime>=%d LIMIT 1", pg, -tm
);
if( tm == -atoi(azPage[1]) ){
/* Specific versions of wiki text never change... However, the match was
** maybe a bit fuzzy so we only do this stuff if there was a specific
** timestamp specified that actually matches the page timestamp.
*/
cgi_modified_since(tm);
cgi_append_header(mprintf("Last-Modified: %h\r\n",
cgi_rfc822_datestamp(tm)));
}
cgi_set_content_type("text/plain");
cgi_append_content(azPage[0],strlen(azPage[0]));
}
/*
** Return TRUE if it is ok to delete the wiki page named Create by zUser
** at time tm. Rules:
**
** * The Setup user can delete any wiki page at any time.
**
** * Users with Delete privilege can delete wiki created by anonymous
** for up to 24 hours.
**
** * Registered users can delete their own wiki for up to 24 hours.
*/
static int ok_to_delete_wiki(int tm, const char *zUser){
if( g.okSetup ){
return 1;
}
if( g.okDelete && strcmp(zUser,"anonymous")==0 && tm>=time(0)-86400 ){
return 1;
}
if( !g.isAnon && strcmp(zUser,g.zUser)==0 && tm>=time(0)-86400 ){
return 1;
}
return 0;
}
/*
** WEBPAGE: /wiki
**
** View a single page of wiki.
**
** Query parameters are "p" and "t". "p" is the name of the page to
** view. If "p" is omitted, the "WikiIndex" page is shown. "t" is
** the time (seconds since 1970) that determines which page to view.
** If omitted, the current time is substituted for "t".
**
** A history of all versions of the page is displayed if the "t"
** parameter is present and is omitted if absent.
*/
void wiki_page(void){
const char *pg = P("p");
const char *zTime = P("t");
char *zSearch = NULL;
int doDiff = atoi(PD("diff","0"));
int tm;
int i;
char **azPage; /* Query result: page to display */
char **azHist = 0; /* Query result: history of the page */
int isLocked;
char *zTimeFmt; /* Human readable translation of "t" parameter */
char *zTruncTime = 0;
char *zTruncTimeFmt = 0;
int truncCnt = 0;
int overload;
login_check_credentials();
if( !g.okRdWiki ){ login_needed(); return; }
overload = throttle(0,0);
if( overload ){
zTime = 0;
doDiff = 0;
}
db_add_functions();
if( zTime==0 || (tm = atoi(zTime))==0 ){
time_t now;
time(&now);
tm = now;
}
if( pg==0 || is_wiki_name(pg)!=strlen(pg) ){
pg = "WikiIndex";
}
azPage = db_query(
"SELECT -invtime, locked, who, ipaddr, text "
"FROM wiki WHERE name='%q' AND invtime>=%d LIMIT 2", pg, -tm
);
if( azPage[0]==0 || azPage[5]==0 ){ doDiff = 0; }
if( zTime && !doDiff ){
zTimeFmt = db_short_query("SELECT ldate(%d)",tm);
azHist = db_query(
"SELECT ldate(-invtime), who, -invtime FROM wiki "
"WHERE name='%q'", pg
);
}
isLocked = azPage[0] && atoi(azPage[1])!=0;
if( pg && strcmp(pg,"WikiIndex")!=0 ){
common_standard_menu(0, "search?w=1");
}else{
common_standard_menu("wiki", "search?w=1");
}
common_add_action_item("wikitoc", "Contents");
if( azPage[0] && g.okWiki && attachment_max()>0 && !overload ){
common_add_action_item( mprintf("attach_add?tn=%t",pg), "Attach");
}
if( zTime==0 && (g.okAdmin || (g.okWiki && !isLocked)) && !overload ){
common_add_action_item( mprintf("wikiedit?p=%t", pg), "Edit");
}
if( zTime==0 && azPage[0] && azPage[5] && !overload ){
common_add_action_item(mprintf("wiki?p=%t&t=%t", pg, azPage[0]), "History");
}
if( !overload ){
common_add_action_item(mprintf("wiki.txt?p=%t&t=%t", pg, azPage[0]),"Text");
}
if( doDiff ){
common_add_action_item(mprintf("wiki?p=%t&t=%t",pg,azPage[0]), "No-Diff");
}else if( azPage[0] && azPage[5] ){
common_add_action_item(mprintf("wiki?p=%t&t=%t&diff=1",pg,azPage[0]),
"Diff");
}
if( azPage[0] && !isLocked && ok_to_delete_wiki(atoi(azPage[0]), azPage[2]) ){
const char *zLink;
if( zTime==0 ){
zLink = mprintf("wikidel?p=%t", pg);
}else{
zLink = mprintf("wikidel?p=%t&t=%d", pg, atoi(azPage[0]));
}
common_add_action_item( zLink, "Delete");
}
zSearch = mprintf("search?s=%t&w=1", pg);
common_link_header(zSearch,wiki_expand_name(pg));
if( zTime && !doDiff ){
@ <table align="right" cellspacing=2 cellpadding=0 border=0
@ bgcolor="%s(BORDER1)" class="border1">
@ <tr><td>
@ <table width="100%%" cellspacing=1 cellpadding=5 border=0
@ bgcolor="%s(BG1)" class="bkgnd1">
@ <tr><th bgcolor="%s(BG1)" class="bkgnd1">Page History</th></tr>
@ </table>
@ </td></tr>
@ <tr><td>
@ <table width="100%%" cellspacing=1 cellpadding=5 border=0
@ bgcolor="white">
@ <tr><td>
for(i=0; azHist[i]; i+=3){
if( azPage[0] && strcmp(azHist[i+2],azPage[0])==0 ){
@ <nobr><b>%h(azHist[i]) %h(azHist[i+1])</b><nobr><br>
if( i>0 && g.okAdmin ){
zTruncTime = azHist[i+2];
zTruncTimeFmt = azHist[i];
truncCnt = 1;
}
}else{
@ <nobr><a href="wiki?p=%h(pg)&t=%h(azHist[i+2])">
@ %h(azHist[i]) %h(azHist[i+1])</a> </nobr><br>
if( zTruncTime ) truncCnt++;
}
}
@ <p><nobr><a href="wiki?p=%h(pg)">Turn Off History</a></nobr></p>
@ </td></tr>
@ </table>
@ </td></tr>
@ </table>
/* @ <p><big><b>%h(wiki_expand_name(pg)) </b></big> */
/* @ <small><i>(as of %h(zTimeFmt))</i></small></p> */
}else{
/* @ <p><big><b>%h(wiki_expand_name(pg))</b></big></p> */
}
if( doDiff ){
diff_strings(3,azPage[9], azPage[4]);
}else if( azPage[0] ){
char *zLinkSuffix;
zLinkSuffix = zTime ? mprintf("&%h",zTime) : "";
output_wiki(azPage[4], zLinkSuffix, pg);
isLocked = atoi(azPage[1]);
attachment_html(pg,
"<h3>Attachments:</h3>\n<blockquote>",
"</blockquote>"
);
} else {
@ <i>This page has not been created...</i>
isLocked = 0;
}
common_footer();
}
/*
** WEBPAGE: /wikidiff
**
** Display the difference between two wiki pages
*/
void wiki_diff(void){
}
/*
** WEBPAGE: /wikiedit
**
** Edit a page of wiki.
*/
void wikiedit_page(void){
const char *pg = P("p");
const char *text = P("x");
char **az;
int isLocked;
login_check_credentials();
throttle(1,1);
if( pg==0 || is_wiki_name(pg)!=strlen(pg) ){
pg = "WikiIndex";
}
az = db_query(
"SELECT invtime, locked, who, ipaddr, text "
"FROM wiki WHERE name='%q' LIMIT 1", pg
);
isLocked = az[0] ? atoi(az[1]) : 0;
if( !g.okAdmin && (!g.okWiki || isLocked) ){
cgi_redirect(mprintf("wiki?p=%t", pg));
}
if( g.okAdmin && az[0] && P("lock")!=0 ){
isLocked = !isLocked;
db_execute("UPDATE wiki SET locked=%d WHERE name='%q'", isLocked, pg);
if( text && strcmp(remove_blank_lines(text),remove_blank_lines(az[4]))==0 ){
cgi_redirect(mprintf("wiki?p=%t",pg));
return;
}
}
if( P("submit")!=0 && text!=0 ){
time_t now;
const char *zIp = getenv("REMOTE_ADDR");
if( zIp==0 ){ zIp = ""; }
time(&now);
db_execute(
"INSERT INTO wiki(name,invtime,locked,who,ipaddr,text) "
"VALUES('%q',%d,%d,'%q','%q','%q')",
pg, -(int)now, isLocked, g.zUser, zIp, remove_blank_lines(text)
);
cgi_redirect(mprintf("wiki?p=%t", pg));
return;
}
if( text==0 ) text = az[0] ? az[4] : "";
text = remove_blank_lines(text);
common_add_action_item( mprintf("wiki?p=%t",pg), "Cancel");
common_header("Edit Wiki %h", pg);
@ <p><big><b>Edit: "%h(wiki_expand_name(pg))"</b></big></p>
@ <form action="wikiedit" method="POST">
@ <input type="hidden" name="p" value="%h(pg)">
@ Make changes to the document text below.
@ See <a href="#formatting">Formatting Hints</a>.
@ <br><textarea cols=80 rows=30 name="x" wrap="physical">
if( text ){
@ %h(text)
}
@ </textarea><br>
if( g.okAdmin ){
if( isLocked ){
@ <input type="submit" name="lock" value="Unlock Page">
@ This page is currently locked, meaning only administrators
@ can edit it.<br>
}else{
@ <input type="submit" name="lock" value="Lock Page">
@ This page is currently unlocked. Anyone can edit it.<br>
}
}
if( P("preview") ){
@ <input type="submit" name="submit" value="Submit Changes As Shown">
}
@ <input type="submit" name="preview" value="Preview Your Changes">
@ </form>
if( P("preview") ){
@ <p>The following is what the page will look like:</p>
@ <p><table border=2 cellpadding=5 width="100%%"><tr><td>
output_wiki(text,"",pg);
@ </td></tr></table></p><br>
}
attachment_html(pg,
"<hr><h3>Attachments:</h3>\n<blockquote>",
"</blockquote>"
);
if( !P("preview") ){
@ <a name="formatting">
@ <hr>
@ <h3>Formatting Hints</h3>
append_formatting_hints();
}
common_footer();
}
/*
** WEBPAGE: /wikitoc
**
** Show a wiki table of contents.
*/
void wikitoc_page(void){
int i;
char **az;
const char *zOrderBy = "1";
const char *zDesc = "";
login_check_credentials();
throttle(0,0);
if( !g.okRdWiki ){ login_needed(); return; }
if( P("ctime") ){
zOrderBy = "min(-invtime)";
}else if( P("mtime") ){
zOrderBy = "max(-invtime)";
}
if( P("desc") ){
zDesc = " DESC";
}
db_add_functions();
az = db_query(
"SELECT name, ldate(min(-invtime)), ldate(max(-invtime)) FROM wiki "
"GROUP BY name ORDER BY %s%s", zOrderBy, zDesc
);
common_standard_menu("wiki", "search?w=1");
common_header("Wiki Table Of Contents");
@ <table>
@ <tr>
@ <th bgcolor="%s(BG3)" class="bkgnd3">
@ <a href="%h(g.zPath)">Page Name</th><th width="20"></th>
@ <th bgcolor="%s(BG3)" class="bkgnd3">
@ <a href="%h(g.zPath)?ctime=1&desc=1">Created</th><th width="20"></th>
@ <th bgcolor="%s(BG3)" class="bkgnd3">
@ <a href="%h(g.zPath)?mtime=1&desc=1">Last Modified</a></th>
@ </tr>
for(i=0; az[i]; i+=3){
@ <tr>
@ <td><a href="wiki?p=%h(az[i])">%h(az[i])</a></td><td></td>
@ <td>%h(az[i+1])</td><td></td><td>%h(az[i+2])</td>
@ </tr>
}
@ </table>
common_footer();
}
/*
** WEBPAGE: /wikidel
**
** The confirmation page for deleting a page of wiki.
*/
void wikidel_page(void){
const char *pg = P("p");
const char *zTime = P("t");
char *zTimeFmt;
const char *zIP = 0;
int nBefore, nAfter, nSimilar;
int tm = 0;
int isLocked;
login_check_credentials();
db_add_functions();
if( pg==0 || is_wiki_name(pg)!=strlen(pg) ){
login_needed();
return;
}
if( zTime==0 || (tm = atoi(zTime))==0 ){
zTime = db_short_query(
"SELECT max(-invtime) FROM wiki WHERE name='%q'", pg);
if( zTime==0 || (tm = atoi(zTime))==0 ){
cgi_redirect("index");
}
}
if( !g.okSetup ){
const char *zUser = db_short_query(
"SELECT who FROM wiki WHERE name='%q' AND invtime=%d", pg, -tm);
if( !ok_to_delete_wiki(tm, zUser) ){
login_needed();
return;
}
}
zIP = db_short_query(
"SELECT ipaddr FROM wiki WHERE name='%q' AND invtime=%d", pg, -tm);
nBefore = atoi( db_short_query(
"SELECT count(*) FROM wiki WHERE name='%q' AND invtime>%d", pg, -tm));
nAfter = atoi( db_short_query(
"SELECT count(*) FROM wiki WHERE name='%q' AND invtime<%d", pg, -tm));
nSimilar = atoi( db_short_query(
"SELECT count(*) FROM wiki WHERE invtime>=%d AND invtime<=%d "
"AND ipaddr='%q'", -3600-tm, 3600-tm, zIP));
zTimeFmt = db_short_query("SELECT ldate(%d)", tm);
isLocked = atoi( db_short_query(
"SELECT locked FROM wiki WHERE name='%q' LIMIT 1", pg));
common_add_action_item(
zTime ? mprintf("wiki?p=%t&t=%d", pg, tm) : mprintf("wiki?p=%t", pg),
"Cancel"
);
common_header("Verify Delete");
@ <p><big><b>Delete Wiki Page "%h(wiki_expand_name(pg))"?</b></big></p>
@ <p>All delete actions are irreversible. Make your choice carefully!</p>
@ <form action="wikidodel" method="GET">
@ <input type="hidden" name="p" value="%h(pg)">
if( P("t") ){
@ <input type="hidden" name="t" value="%d(tm)">
}
@ <input type="hidden" name="t2" value="%d(tm)">
@ <table border=0 cellpadding=5>
@
if( !isLocked && (g.okSetup || nBefore+nAfter==0) ){
@ <tr><td align="right">
if( nBefore==0 && nAfter==0 ){
@ <input type="submit" name="all" value="Yes">
}else{
@ <input type="submit" name="all" value="All">
}
@ </td><td>
@ Delete this page with all its history.
@ </td></tr>
@
}
if( nBefore>0 && nAfter>0 && g.okSetup ){
@
@ <tr><td align="right">
@ <input type="submit" name="after" value="Older">
@ </td><td>
@ Delete %d(nBefore+1) historical version(s) from %s(zTimeFmt) and older
@ but retain the %d(nAfter) most recent version(s) of the page.
@ </td></tr>
}
if( nBefore+nAfter>0 ){
@
@ <tr><td align="right">
@ <input type="submit" name="one" value="One">
@ </td><td>
@ Delete a single page from %s(zTimeFmt)
@ but retain the %d(nBefore+nAfter) other version(s) of the page.
@ </td></tr>
}
if( zIP && zIP[0] && nSimilar>1 ){
@
@ <tr><td align="right">
@ <input type="submit" name="similar" value="Similar">
@ <input type="hidden" name="ip" value="%s(zIP)">
@ </td><td>
@ Delete %d(nSimilar) changes to this and other wiki pages
@ from IP address (%s(zIP)) that occur
@ within one hour of %s(zTimeFmt).
@ </td></tr>
}
@
@ <tr><td align="right">
@ <input type="submit" name="cancel" value="Cancel">
@ </td><td>
@ Do not delete anything.
@ </td></tr>
@ </table>
@ </form>
common_footer();
}
/*
** WEBPAGE: /wikidodel
**
** Do the actual work of deleting a page. Nothing is displayed.
** After the delete is accomplished, we redirect to a different page.
*/
void wikidodel_page(void){
const char *pg = P("p");
const char *t = P("t");
const char *t2 = P("t2");
char *zLast;
const char *zRestrict;
login_check_credentials();
if( pg==0 || is_wiki_name(pg)!=strlen(pg) || t2==0 ){
login_needed();
return;
}
if( P("cancel") ){
if( t==0 ){
cgi_redirect(mprintf("wiki?p=%t",pg));
}else{
cgi_redirect(mprintf("wiki?p=%t&t=%t",pg,t));
}
return;
}
db_add_functions();
if( g.okSetup ){
/* The Setup user can delete anything */
zRestrict = "";
}else if( g.okDelete ){
/* Make sure users with Delete privilege but without Setup privilege
** can only delete wiki added by anonymous within the past 24 hours.
*/
zRestrict = " AND (who='anonymous' OR who=user()) AND invtime<86400-now()";
}else if( g.isAnon ){
/* Anonymous user without Delete privilege cannot delete anything */
login_needed();
return;
}else{
/* What is left is registered users without Delete privilege. They
** can delete the things that they themselves have added within 24
** hours. */
zRestrict = " AND who=user() AND invtime<=86400-now()";
}
if( P("all") ){
db_execute(
"BEGIN;"
"DELETE FROM wiki WHERE name='%q'%s;"
"DELETE FROM attachment WHERE tn='%q' AND "
"(SELECT count(*) FROM wiki WHERE name='%q')==0;"
"COMMIT",
pg, zRestrict, pg, pg
);
cgi_redirect("wiki?p=WikiIndex");
return;
}
if( P("similar") ){
db_execute("DELETE FROM wiki WHERE invtime>=%d AND invtime<=%d "
"AND ipaddr='%q'%s",
-3600-atoi(t2), 3600-atoi(t2), P("ip"), zRestrict);
}else if( P("one") ){
db_execute("DELETE FROM wiki WHERE name='%q' AND invtime=%d%s",
pg, -atoi(t2), zRestrict);
}else if( P("after") && g.okSetup ){
db_execute("DELETE FROM wiki WHERE name='%q' AND invtime>=%d",pg,-atoi(t2));
}
zLast = db_short_query("SELECT max(-invtime) FROM wiki WHERE name='%q'",pg);
if( zLast ){
cgi_redirect(mprintf("wiki?p=%t&t=%t",pg,zLast));
}else{
cgi_redirect(mprintf("wiki?p=%t",pg));
}
}