1 /*
2 ** Copyright (c) 2007 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used to create a RSS feed for the CGI interface.
19 */
20 #include "config.h"
21 #include <time.h>
22 #include "rss.h"
23 #include <assert.h>
24 
25 /*
26 ** WEBPAGE: timeline.rss
27 ** URL:  /timeline.rss?y=TYPE&n=LIMIT&tkt=HASH&tag=TAG&wiki=NAME&name=FILENAME
28 **
29 ** Produce an RSS feed of the timeline.
30 **
31 ** TYPE may be: all, ci (show check-ins only), t (show ticket changes only),
32 ** w (show wiki only), e (show tech notes only), f (show forum posts only),
33 ** g (show tag/branch changes only).
34 **
35 ** LIMIT is the number of items to show.
36 **
37 ** tkt=HASH filters for only those events for the specified ticket. tag=TAG
38 ** filters for a tag, and wiki=NAME for a wiki page. Only one may be used.
39 **
40 ** In addition, name=FILENAME filters for a specific file. This may be
41 ** combined with one of the other filters (useful for looking at a specific
42 ** branch).
43 */
44 
page_timeline_rss(void)45 void page_timeline_rss(void){
46   Stmt q;
47   int nLine=0;
48   char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
49   Blob bSQL;
50   const char *zType = PD("y","all"); /* Type of events.  All if NULL */
51   const char *zTicketUuid = PD("tkt",NULL);
52   const char *zTag = PD("tag",NULL);
53   const char *zFilename = PD("name",NULL);
54   const char *zWiki = PD("wiki",NULL);
55   int nLimit = atoi(PD("n","20"));
56   int nTagId;
57   const char zSQL1[] =
58     @ SELECT
59     @   blob.rid,
60     @   uuid,
61     @   event.mtime,
62     @   coalesce(ecomment,comment),
63     @   coalesce(euser,user),
64     @   (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
65     @   (SELECT count(*) FROM plink WHERE cid=blob.rid),
66     @   (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
67     @     WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
68     @       AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags
69     @ FROM event, blob
70     @ WHERE blob.rid=event.objid
71   ;
72 
73   login_check_credentials();
74   if( !g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki ){
75     return;
76   }
77 
78   blob_zero(&bSQL);
79   blob_append( &bSQL, zSQL1, -1 );
80 
81   if( zType[0]!='a' ){
82     if( zType[0]=='c' && !g.perm.Read ) zType = "x";
83     if( zType[0]=='w' && !g.perm.RdWiki ) zType = "x";
84     if( zType[0]=='t' && !g.perm.RdTkt ) zType = "x";
85     blob_append_sql(&bSQL, " AND event.type=%Q", zType);
86   }else{
87     if( !g.perm.Read ){
88       if( g.perm.RdTkt && g.perm.RdWiki ){
89         blob_append(&bSQL, " AND event.type!='ci'", -1);
90       }else if( g.perm.RdTkt ){
91         blob_append(&bSQL, " AND event.type=='t'", -1);
92 
93       }else{
94         blob_append(&bSQL, " AND event.type=='w'", -1);
95       }
96     }else if( !g.perm.RdWiki ){
97       if( g.perm.RdTkt ){
98         blob_append(&bSQL, " AND event.type!='w'", -1);
99       }else{
100         blob_append(&bSQL, " AND event.type=='ci'", -1);
101       }
102     }else if( !g.perm.RdTkt ){
103       assert( !g.perm.RdTkt && g.perm.Read && g.perm.RdWiki );
104       blob_append(&bSQL, " AND event.type!='t'", -1);
105     }
106   }
107 
108   if( zTicketUuid ){
109     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
110       zTicketUuid);
111     if ( nTagId==0 ){
112       nTagId = -1;
113     }
114   }else if( zTag ){
115     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q*'",
116       zTag);
117     if ( nTagId==0 ){
118       nTagId = -1;
119     }
120   }else if( zWiki ){
121     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'wiki-%q*'",
122       zWiki);
123     if ( nTagId==0 ){
124       nTagId = -1;
125     }
126   }else{
127     nTagId = 0;
128   }
129 
130   if( nTagId==-1 ){
131     blob_append_sql(&bSQL, " AND 0");
132   }else if( nTagId!=0 ){
133     blob_append_sql(&bSQL, " AND (EXISTS(SELECT 1 FROM tagxref"
134       " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid))", nTagId);
135   }
136 
137   if( zFilename ){
138     blob_append_sql(&bSQL,
139       " AND (SELECT mlink.fnid FROM mlink WHERE event.objid=mlink.mid) IN (SELECT fnid FROM filename WHERE name=%Q %s)",
140         zFilename, filename_collation()
141     );
142   }
143 
144   blob_append( &bSQL, " ORDER BY event.mtime DESC", -1 );
145 
146   cgi_set_content_type("application/rss+xml");
147 
148   zProjectName = db_get("project-name", 0);
149   if( zProjectName==0 ){
150     zFreeProjectName = zProjectName = mprintf("Fossil source repository for: %s",
151       g.zBaseURL);
152   }
153   zProjectDescr = db_get("project-description", 0);
154   if( zProjectDescr==0 ){
155     zProjectDescr = zProjectName;
156   }
157 
158   zPubDate = cgi_rfc822_datestamp(time(NULL));
159 
160   @ <?xml version="1.0"?>
161   @ <rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
162   @   <channel>
163   @     <title>%h(zProjectName)</title>
164   @     <link>%s(g.zBaseURL)</link>
165   @     <description>%h(zProjectDescr)</description>
166   @     <pubDate>%s(zPubDate)</pubDate>
167   @     <generator>Fossil version %s(MANIFEST_VERSION) %s(MANIFEST_DATE)</generator>
168   free(zPubDate);
169   db_prepare(&q, "%s", blob_sql_text(&bSQL));
170   blob_reset( &bSQL );
171   while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
172     const char *zId = db_column_text(&q, 1);
173     const char *zCom = db_column_text(&q, 3);
174     const char *zAuthor = db_column_text(&q, 4);
175     char *zPrefix = "";
176     char *zSuffix = 0;
177     char *zDate;
178     int nChild = db_column_int(&q, 5);
179     int nParent = db_column_int(&q, 6);
180     const char *zTagList = db_column_text(&q, 7);
181     time_t ts;
182 
183     if( zTagList && zTagList[0]==0 ) zTagList = 0;
184     ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
185     zDate = cgi_rfc822_datestamp(ts);
186 
187     if( nParent>1 && nChild>1 ){
188       zPrefix = "*MERGE/FORK* ";
189     }else if( nParent>1 ){
190       zPrefix = "*MERGE* ";
191     }else if( nChild>1 ){
192       zPrefix = "*FORK* ";
193     }
194 
195     if( zTagList ){
196       zSuffix = mprintf(" (tags: %s)", zTagList);
197     }
198 
199     @     <item>
200     @       <title>%s(zPrefix)%h(zCom)%h(zSuffix)</title>
201     @       <link>%s(g.zBaseURL)/info/%s(zId)</link>
202     @       <description>%s(zPrefix)%h(zCom)%h(zSuffix)</description>
203     @       <pubDate>%s(zDate)</pubDate>
204     @       <dc:creator>%h(zAuthor)</dc:creator>
205     @       <guid>%s(g.zBaseURL)/info/%s(zId)</guid>
206     @     </item>
207     free(zDate);
208     free(zSuffix);
209     nLine++;
210   }
211 
212   db_finalize(&q);
213   @   </channel>
214   @ </rss>
215 
216   if( zFreeProjectName != 0 ){
217     free( zFreeProjectName );
218   }
219 }
220 
221 /*
222 ** COMMAND: rss*
223 **
224 ** Usage: %fossil rss ?OPTIONS?
225 **
226 ** The CLI variant of the /timeline.rss page, this produces an RSS
227 ** feed of the timeline to stdout. Options:
228 **
229 **   -type|y FLAG    May be: all (default), ci (show check-ins only),
230 **                   t (show tickets only),  w (show wiki only).
231 **
232 **   -limit|n LIMIT  The maximum number of items to show.
233 **
234 **   -tkt HASH       Filter for only those events for the specified ticket.
235 **
236 **   -tag TAG        Filter for a tag
237 **
238 **   -wiki NAME      Filter on a specific wiki page.
239 **
240 ** Only one of -tkt, -tag, or -wiki may be used.
241 **
242 **   -name FILENAME  Filter for a specific file. This may be combined
243 **                   with one of the other filters (useful for looking
244 **                   at a specific branch).
245 **
246 **   -url STRING     Set the RSS feed's root URL to the given string.
247 **                   The default is "URL-PLACEHOLDER" (without quotes).
248 */
cmd_timeline_rss(void)249 void cmd_timeline_rss(void){
250   Stmt q;
251   int nLine=0;
252   char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
253   Blob bSQL;
254   const char *zType = find_option("type","y",1); /* Type of events.  All if NULL */
255   const char *zTicketUuid = find_option("tkt",NULL,1);
256   const char *zTag = find_option("tag",NULL,1);
257   const char *zFilename = find_option("name",NULL,1);
258   const char *zWiki = find_option("wiki",NULL,1);
259   const char *zLimit = find_option("limit", "n",1);
260   const char *zBaseURL = find_option("url", NULL, 1);
261   int nLimit = atoi( (zLimit && *zLimit) ? zLimit : "20" );
262   int nTagId;
263   const char zSQL1[] =
264     @ SELECT
265     @   blob.rid,
266     @   uuid,
267     @   event.mtime,
268     @   coalesce(ecomment,comment),
269     @   coalesce(euser,user),
270     @   (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
271     @   (SELECT count(*) FROM plink WHERE cid=blob.rid),
272     @   (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
273     @     WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
274     @       AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags
275     @ FROM event, blob
276     @ WHERE blob.rid=event.objid
277   ;
278   if(!zType || !*zType){
279     zType = "all";
280   }
281   if(!zBaseURL || !*zBaseURL){
282     zBaseURL = "URL-PLACEHOLDER";
283   }
284 
285   db_find_and_open_repository(0, 0);
286 
287   /* We should be done with options.. */
288     verify_all_options();
289 
290   blob_zero(&bSQL);
291   blob_append( &bSQL, zSQL1, -1 );
292 
293   if( zType[0]!='a' ){
294     blob_append_sql(&bSQL, " AND event.type=%Q", zType);
295   }
296 
297   if( zTicketUuid ){
298     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
299       zTicketUuid);
300     if ( nTagId==0 ){
301       nTagId = -1;
302     }
303   }else if( zTag ){
304     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q*'",
305       zTag);
306     if ( nTagId==0 ){
307       nTagId = -1;
308     }
309   }else if( zWiki ){
310     nTagId = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'wiki-%q*'",
311       zWiki);
312     if ( nTagId==0 ){
313       nTagId = -1;
314     }
315   }else{
316     nTagId = 0;
317   }
318 
319   if( nTagId==-1 ){
320     blob_append_sql(&bSQL, " AND 0");
321   }else if( nTagId!=0 ){
322     blob_append_sql(&bSQL, " AND (EXISTS(SELECT 1 FROM tagxref"
323       " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid))", nTagId);
324   }
325 
326   if( zFilename ){
327     blob_append_sql(&bSQL,
328       " AND (SELECT mlink.fnid FROM mlink WHERE event.objid=mlink.mid) IN (SELECT fnid FROM filename WHERE name=%Q %s)",
329         zFilename, filename_collation()
330     );
331   }
332 
333   blob_append( &bSQL, " ORDER BY event.mtime DESC", -1 );
334 
335   zProjectName = db_get("project-name", 0);
336   if( zProjectName==0 ){
337     zFreeProjectName = zProjectName = mprintf("Fossil source repository for: %s",
338       zBaseURL);
339   }
340   zProjectDescr = db_get("project-description", 0);
341   if( zProjectDescr==0 ){
342     zProjectDescr = zProjectName;
343   }
344 
345   zPubDate = cgi_rfc822_datestamp(time(NULL));
346 
347   fossil_print("<?xml version=\"1.0\"?>");
348   fossil_print("<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" version=\"2.0\">");
349   fossil_print("<channel>\n");
350   fossil_print("<title>%h</title>\n", zProjectName);
351   fossil_print("<link>%s</link>\n", zBaseURL);
352   fossil_print("<description>%h</description>\n", zProjectDescr);
353   fossil_print("<pubDate>%s</pubDate>\n", zPubDate);
354   fossil_print("<generator>Fossil version %s %s</generator>\n",
355                MANIFEST_VERSION, MANIFEST_DATE);
356   free(zPubDate);
357   db_prepare(&q, "%s", blob_sql_text(&bSQL));
358   blob_reset( &bSQL );
359   while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
360     const char *zId = db_column_text(&q, 1);
361     const char *zCom = db_column_text(&q, 3);
362     const char *zAuthor = db_column_text(&q, 4);
363     char *zPrefix = "";
364     char *zSuffix = 0;
365     char *zDate;
366     int nChild = db_column_int(&q, 5);
367     int nParent = db_column_int(&q, 6);
368     const char *zTagList = db_column_text(&q, 7);
369     time_t ts;
370 
371     if( zTagList && zTagList[0]==0 ) zTagList = 0;
372     ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
373     zDate = cgi_rfc822_datestamp(ts);
374 
375     if( nParent>1 && nChild>1 ){
376       zPrefix = "*MERGE/FORK* ";
377     }else if( nParent>1 ){
378       zPrefix = "*MERGE* ";
379     }else if( nChild>1 ){
380       zPrefix = "*FORK* ";
381     }
382 
383     if( zTagList ){
384       zSuffix = mprintf(" (tags: %s)", zTagList);
385     }
386 
387     fossil_print("<item>");
388     fossil_print("<title>%s%h%h</title>\n", zPrefix, zCom, zSuffix);
389     fossil_print("<link>%s/info/%s</link>\n", zBaseURL, zId);
390     fossil_print("<description>%s%h%h</description>\n", zPrefix, zCom, zSuffix);
391     fossil_print("<pubDate>%s</pubDate>\n", zDate);
392     fossil_print("<dc:creator>%h</dc:creator>\n", zAuthor);
393     fossil_print("<guid>%s/info/%s</guid>\n", g.zBaseURL, zId);
394     fossil_print("</item>\n");
395     free(zDate);
396     free(zSuffix);
397     nLine++;
398   }
399 
400   db_finalize(&q);
401   fossil_print("</channel>\n");
402   fossil_print("</rss>\n");
403 
404   if( zFreeProjectName != 0 ){
405     free( zFreeProjectName );
406   }
407 }
408