Fossil SCM

Include rendered forum HTML in RSS with absolute links and latest-edit filtering.

vor0nwe 2026-03-06 15:41 trunk
Commit 43267589414bec7c0bf7c7afd631997be64eead6202101631c9927e16c05a1de
3 files changed +14 +325 -18 +59
+14
--- src/forum.c
+++ src/forum.c
@@ -19,10 +19,12 @@
1919
*/
2020
#include "config.h"
2121
#include <assert.h>
2222
#include "forum.h"
2323
24
+void wiki_convert_to_html(struct Blob*, const char*, const char*, int);
25
+
2426
/*
2527
** Default to using Markdown markup
2628
*/
2729
#define DEFAULT_FORUM_MIMETYPE "text/x-markdown"
2830
@@ -681,10 +683,22 @@
681683
}
682684
if( zClass ){
683685
@ </div>
684686
}
685687
}
688
+
689
+/*
690
+** Render forum post content into an output blob as HTML.
691
+** The output is suitable for embedding in RSS content:encoded.
692
+*/
693
+void forum_render_to_html(
694
+ Blob *pOut, /* Output HTML is appended here */
695
+ const char *zMimetype, /* Mimetype of the message */
696
+ const char *zContent /* Content of the message */
697
+){
698
+ wiki_convert_to_html(pOut, zMimetype, zContent, DOCSRC_FORUM);
699
+}
686700
687701
/*
688702
** Compute a display name from a login name.
689703
**
690704
** If the input login is found in the USER table, then check the USER.INFO
691705
--- src/forum.c
+++ src/forum.c
@@ -19,10 +19,12 @@
19 */
20 #include "config.h"
21 #include <assert.h>
22 #include "forum.h"
23
 
 
24 /*
25 ** Default to using Markdown markup
26 */
27 #define DEFAULT_FORUM_MIMETYPE "text/x-markdown"
28
@@ -681,10 +683,22 @@
681 }
682 if( zClass ){
683 @ </div>
684 }
685 }
 
 
 
 
 
 
 
 
 
 
 
 
686
687 /*
688 ** Compute a display name from a login name.
689 **
690 ** If the input login is found in the USER table, then check the USER.INFO
691
--- src/forum.c
+++ src/forum.c
@@ -19,10 +19,12 @@
19 */
20 #include "config.h"
21 #include <assert.h>
22 #include "forum.h"
23
24 void wiki_convert_to_html(struct Blob*, const char*, const char*, int);
25
26 /*
27 ** Default to using Markdown markup
28 */
29 #define DEFAULT_FORUM_MIMETYPE "text/x-markdown"
30
@@ -681,10 +683,22 @@
683 }
684 if( zClass ){
685 @ </div>
686 }
687 }
688
689 /*
690 ** Render forum post content into an output blob as HTML.
691 ** The output is suitable for embedding in RSS content:encoded.
692 */
693 void forum_render_to_html(
694 Blob *pOut, /* Output HTML is appended here */
695 const char *zMimetype, /* Mimetype of the message */
696 const char *zContent /* Content of the message */
697 ){
698 wiki_convert_to_html(pOut, zMimetype, zContent, DOCSRC_FORUM);
699 }
700
701 /*
702 ** Compute a display name from a login name.
703 **
704 ** If the input login is found in the USER table, then check the USER.INFO
705
+325 -18
--- src/rss.c
+++ src/rss.c
@@ -19,10 +19,195 @@
1919
*/
2020
#include "config.h"
2121
#include <time.h>
2222
#include "rss.h"
2323
#include <assert.h>
24
+
25
+void forum_render_to_html(struct Blob*, const char*, const char*);
26
+
27
+/*
28
+** Append text to pOut, escaping any CDATA terminators.
29
+*/
30
+static void rss_cdata_append(Blob *pOut, const char *zIn, int nIn){
31
+ const char *zEnd;
32
+ const char *zDelim;
33
+ if( pOut==0 ) return;
34
+ if( zIn==0 ) zIn = "";
35
+ if( nIn<0 ) nIn = (int)strlen(zIn);
36
+ zEnd = zIn + nIn;
37
+ while( zIn<zEnd && (zDelim = strstr(zIn, "]]>") )!=0 ){
38
+ if( zDelim>=zEnd ) break;
39
+ blob_append(pOut, zIn, (int)(zDelim - zIn));
40
+ blob_append_literal(pOut, "]]]]><![CDATA[>");
41
+ zIn = zDelim + 3;
42
+ }
43
+ if( zIn<zEnd ){
44
+ blob_append(pOut, zIn, (int)(zEnd - zIn));
45
+ }
46
+}
47
+
48
+/*
49
+** Return true if zIn looks like an absolute URL.
50
+*/
51
+static int rss_is_absolute_url(const char *zIn, int nIn){
52
+ int i;
53
+ if( zIn==0 || nIn<=0 ) return 0;
54
+ if( nIn>=2 && zIn[0]=='/' && zIn[1]=='/' ) return 1;
55
+ if( !((zIn[0]>='a' && zIn[0]<='z') || (zIn[0]>='A' && zIn[0]<='Z')) ){
56
+ return 0;
57
+ }
58
+ for(i=1; i<nIn; i++){
59
+ char c = zIn[i];
60
+ if( (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9')
61
+ || c=='+' || c=='-' || c=='.' ){
62
+ continue;
63
+ }
64
+ return c==':' ? 1 : 0;
65
+ }
66
+ return 0;
67
+}
68
+
69
+/*
70
+** Return the length of zBase without trailing slashes.
71
+*/
72
+static int rss_trim_base(const char *zBase){
73
+ int n = zBase ? (int)strlen(zBase) : 0;
74
+ while( n>0 && zBase[n-1]=='/' ) n--;
75
+ return n;
76
+}
77
+
78
+/*
79
+** Compute the repository top path from zBase and return it.
80
+*/
81
+static const char *rss_top_from_base(Blob *pTop, const char *zBase){
82
+ const char *z = zBase;
83
+ const char *zSlash = 0;
84
+ int n;
85
+ if( zBase==0 ) return "";
86
+ if( strncmp(zBase, "http://", 7)==0 ){
87
+ z = zBase + 7;
88
+ }else if( strncmp(zBase, "https://", 8)==0 ){
89
+ z = zBase + 8;
90
+ }else{
91
+ return "";
92
+ }
93
+ zSlash = strchr(z, '/');
94
+ if( zSlash==0 ) return "";
95
+ n = (int)strlen(zSlash);
96
+ while( n>1 && zSlash[n-1]=='/' ) n--;
97
+ blob_init(pTop, zSlash, n);
98
+ return blob_str(pTop);
99
+}
100
+
101
+/*
102
+** Append an absolute URL to pOut, using zBase/zTop as the base.
103
+*/
104
+static void rss_append_abs_url(
105
+ Blob *pOut,
106
+ const char *zBase,
107
+ int nBase,
108
+ const char *zTop,
109
+ int nTop,
110
+ const char *zRel,
111
+ int nRel
112
+){
113
+ if( pOut==0 ) return;
114
+ if( zRel==0 || nRel<=0 ) return;
115
+ if( zBase==0 || zBase[0]==0 ){
116
+ blob_append(pOut, zRel, nRel);
117
+ return;
118
+ }
119
+ if( zRel[0]=='#' || rss_is_absolute_url(zRel, nRel) ){
120
+ blob_append(pOut, zRel, nRel);
121
+ return;
122
+ }
123
+ if( zRel[0]=='/' ){
124
+ if( nTop>1 && strncmp(zRel, zTop, nTop)==0 ){
125
+ blob_append(pOut, zBase, nBase);
126
+ blob_append(pOut, zRel + nTop, nRel - nTop);
127
+ }else{
128
+ blob_append(pOut, zBase, nBase);
129
+ blob_append(pOut, zRel, nRel);
130
+ }
131
+ }else{
132
+ blob_append(pOut, zBase, nBase);
133
+ blob_append_char(pOut, '/');
134
+ blob_append(pOut, zRel, nRel);
135
+ }
136
+}
137
+
138
+/*
139
+** Convert relative href/src attributes in zIn to absolute URLs.
140
+*/
141
+static void rss_make_abs_links(
142
+ Blob *pOut,
143
+ const char *zBase,
144
+ const char *zTop,
145
+ const char *zIn,
146
+ int nIn
147
+){
148
+ const char *z = zIn;
149
+ const char *zEnd = zIn + nIn;
150
+ const char *zLast = zIn;
151
+ int nBase = rss_trim_base(zBase);
152
+ int nTop = zTop ? (int)strlen(zTop) : 0;
153
+ if( pOut==0 || zIn==0 ) return;
154
+ while( z<zEnd ){
155
+ int nAttr = 0;
156
+ if( (z>zIn && !(z[-1]==' ' || (z[-1]>='\t' && z[-1]<='\r'))
157
+ && z[-1]!='<') ){
158
+ z++;
159
+ continue;
160
+ }
161
+ if( zEnd - z >= 5
162
+ && (z[0]=='h' || z[0]=='H')
163
+ && (z[1]=='r' || z[1]=='R')
164
+ && (z[2]=='e' || z[2]=='E')
165
+ && (z[3]=='f' || z[3]=='F')
166
+ && z[4]=='='
167
+ ){
168
+ nAttr = 5;
169
+ }else if( zEnd - z >= 4
170
+ && (z[0]=='s' || z[0]=='S')
171
+ && (z[1]=='r' || z[1]=='R')
172
+ && (z[2]=='c' || z[2]=='C')
173
+ && z[3]=='='
174
+ ){
175
+ nAttr = 4;
176
+ }
177
+ if( nAttr==0 ){
178
+ z++;
179
+ continue;
180
+ }
181
+ {
182
+ const char *zVal = z + nAttr;
183
+ const char *zValEnd = zVal;
184
+ char quote = 0;
185
+ if( zVal>=zEnd ) break;
186
+ if( *zVal=='"' || *zVal=='\'' ){
187
+ quote = *zVal;
188
+ zVal++;
189
+ }
190
+ zValEnd = zVal;
191
+ while( zValEnd<zEnd ){
192
+ if( quote ){
193
+ if( *zValEnd==quote ) break;
194
+ }else if( *zValEnd==' ' || (*zValEnd>='\t' && *zValEnd<='\r')
195
+ || *zValEnd=='>' ){
196
+ break;
197
+ }
198
+ zValEnd++;
199
+ }
200
+ blob_append(pOut, zLast, zVal - zLast);
201
+ rss_append_abs_url(pOut, zBase, nBase, zTop, nTop,
202
+ zVal, (int)(zValEnd - zVal));
203
+ zLast = zValEnd;
204
+ z = zValEnd;
205
+ }
206
+ }
207
+ if( zLast<zEnd ) blob_append(pOut, zLast, (int)(zEnd - zLast));
208
+}
24209
25210
/*
26211
** WEBPAGE: timeline.rss
27212
** URL: /timeline.rss?y=TYPE&n=LIMIT&tkt=HASH&tag=TAG&wiki=NAME&name=FILENAME
28213
**
@@ -44,17 +229,20 @@
44229
void page_timeline_rss(void){
45230
Stmt q;
46231
int nLine=0;
47232
char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
48233
Blob bSQL;
234
+ Blob base = BLOB_INITIALIZER;
235
+ Blob top = BLOB_INITIALIZER;
49236
const char *zType = PD("y","all"); /* Type of events. All if NULL */
50237
const char *zTicketUuid = PD("tkt",NULL);
51238
const char *zTag = PD("tag",NULL);
52239
const char *zFilename = PD("name",NULL);
53240
const char *zWiki = PD("wiki",NULL);
54241
int nLimit = atoi(PD("n","20"));
55242
int nTagId;
243
+ int bHasForum;
56244
const char zSQL1[] =
57245
@ SELECT
58246
@ blob.rid,
59247
@ uuid,
60248
@ event.mtime,
@@ -69,16 +257,27 @@
69257
@ FROM event, blob
70258
@ WHERE blob.rid=event.objid
71259
;
72260
73261
login_check_credentials();
74
- if( !g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki ){
262
+ if( !g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum ){
75263
return;
76264
}
265
+ bHasForum = db_table_exists("repository","forumpost");
77266
78267
blob_zero(&bSQL);
79268
blob_append_sql( &bSQL, "%s", zSQL1/*safe-for-%s*/ );
269
+ if( bHasForum ){
270
+ blob_append_sql(&bSQL,
271
+ " AND (event.type!='f' OR event.objid IN ("
272
+ "SELECT fpid FROM forumpost AS f "
273
+ "WHERE NOT EXISTS(SELECT 1 FROM forumpost AS nx "
274
+ "WHERE nx.fprev=f.fpid)))"
275
+ );
276
+ }else{
277
+ blob_append_sql(&bSQL, " AND event.type!='f'");
278
+ }
80279
81280
if( zType[0]!='a' ){
82281
if( zType[0]=='c' && !g.perm.Read ) zType = "x";
83282
else if( (zType[0]=='w' || zType[0]=='e') && !g.perm.RdWiki ) zType = "x";
84283
else if( zType[0]=='t' && !g.perm.RdTkt ) zType = "x";
@@ -151,13 +350,17 @@
151350
if( zProjectDescr==0 ){
152351
zProjectDescr = zProjectName;
153352
}
154353
155354
zPubDate = cgi_rfc822_datestamp(time(NULL));
355
+ blob_append(&base, g.zBaseURL, -1);
356
+ rss_top_from_base(&top, g.zBaseURL);
156357
157358
@ <?xml version="1.0"?>
158
- @ <rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
359
+ @ <rss xmlns:dc="http://purl.org/dc/elements/1.1/" \
360
+ @ xmlns:content="http://purl.org/rss/1.0/modules/content/" \
361
+ @ version="2.0">
159362
@ <channel>
160363
@ <title>%h(zProjectName)</title>
161364
@ <link>%s(g.zBaseURL)</link>
162365
@ <description>%h(zProjectDescr)</description>
163366
@ <pubDate>%s(zPubDate)</pubDate>
@@ -164,35 +367,39 @@
164367
@ <generator>Fossil version %s(MANIFEST_VERSION) %s(MANIFEST_DATE)</generator>
165368
free(zPubDate);
166369
db_prepare(&q, "%s", blob_sql_text(&bSQL));
167370
blob_reset( &bSQL );
168371
while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
372
+ int rid = db_column_int(&q, 0);
169373
const char *zId = db_column_text(&q, 1);
170374
const char *zEType = db_column_text(&q, 3);
171375
const char *zCom = db_column_text(&q, 4);
172376
const char *zAuthor = db_column_text(&q, 5);
173377
char *zPrefix = "";
174378
char *zSuffix = 0;
175379
char *zDate;
176
- int nChild = db_column_int(&q, 5);
380
+ int nChild = db_column_int(&q, 6);
177381
int nParent = db_column_int(&q, 7);
178382
const char *zTagList = db_column_text(&q, 8);
383
+ Manifest *pPost = 0;
384
+ Blob contentHtml = BLOB_INITIALIZER;
385
+ int bForumContent = 0;
179386
time_t ts;
180387
181388
if( zTagList && zTagList[0]==0 ) zTagList = 0;
182389
ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
183390
zDate = cgi_rfc822_datestamp(ts);
184391
185
- if('c'==zEType[0]){
392
+ if( zEType[0]=='c' ){
186393
if( nParent>1 && nChild>1 ){
187394
zPrefix = "*MERGE/FORK* ";
188395
}else if( nParent>1 ){
189396
zPrefix = "*MERGE* ";
190397
}else if( nChild>1 ){
191398
zPrefix = "*FORK* ";
192399
}
193
- }else if('w'==zEType[0]){
400
+ }else if( zEType[0]=='w' ){
194401
switch(zCom ? zCom[0] : 0){
195402
case ':': zPrefix = "Edit wiki page: "; break;
196403
case '+': zPrefix = "Add wiki page: "; break;
197404
case '-': zPrefix = "Delete wiki page: "; break;
198405
}
@@ -201,24 +408,60 @@
201408
202409
if( zTagList ){
203410
zSuffix = mprintf(" (tags: %s)", zTagList);
204411
}
205412
413
+ if( zEType[0]=='f' ){
414
+ if( !g.perm.ModForum && content_is_private(rid) ){
415
+ free(zDate);
416
+ free(zSuffix);
417
+ continue;
418
+ }
419
+ pPost = manifest_get(rid, CFTYPE_FORUM, 0);
420
+ if( pPost ){
421
+ forum_render_to_html(&contentHtml, pPost->zMimetype, pPost->zWiki);
422
+ if( blob_size(&contentHtml)>0 ){
423
+ Blob normalized = BLOB_INITIALIZER;
424
+ rss_make_abs_links(&normalized, blob_str(&base),
425
+ blob_str(&top), blob_str(&contentHtml),
426
+ blob_size(&contentHtml));
427
+ blob_reset(&contentHtml);
428
+ blob_append(&contentHtml, blob_str(&normalized),
429
+ blob_size(&normalized));
430
+ blob_reset(&normalized);
431
+ bForumContent = 1;
432
+ }
433
+ }
434
+ }
206435
@ <item>
207436
@ <title>%s(zPrefix)%h(zCom)%h(zSuffix)</title>
208437
@ <link>%s(g.zBaseURL)/info/%s(zId)</link>
209438
@ <description>%s(zPrefix)%h(zCom)%h(zSuffix)</description>
210439
@ <pubDate>%s(zDate)</pubDate>
211440
@ <dc:creator>%h(zAuthor)</dc:creator>
212441
@ <guid>%s(g.zBaseURL)/info/%s(zId)</guid>
442
+ if( bForumContent ){
443
+ Blob cdata = BLOB_INITIALIZER;
444
+ @ <dc:format>text/html</dc:format>
445
+ @ <content:encoded><![CDATA[
446
+ rss_cdata_append(&cdata, blob_str(&contentHtml),
447
+ blob_size(&contentHtml));
448
+ cgi_append_content(blob_str(&cdata), blob_size(&cdata));
449
+ blob_reset(&cdata);
450
+ @ ]]></content:encoded>
451
+ }
213452
@ </item>
453
+ if( pPost ) manifest_destroy(pPost);
454
+ blob_reset(&contentHtml);
214455
free(zDate);
215456
free(zSuffix);
216457
nLine++;
217458
}
218459
219460
db_finalize(&q);
461
+ blob_reset(&base);
462
+ blob_reset(&top);
220463
@ </channel>
221464
@ </rss>
222465
223466
if( zFreeProjectName != 0 ){
224467
free( zFreeProjectName );
@@ -233,11 +476,12 @@
233476
** The CLI variant of the /timeline.rss page, this produces an RSS
234477
** feed of the timeline to stdout.
235478
**
236479
** Options:
237480
** -type|y FLAG May be: all (default), ci (show check-ins only),
238
-** t (show tickets only), w (show wiki only)
481
+** t (show tickets only), w (show wiki only),
482
+** e (show tech notes only), f (show forum posts only)
239483
**
240484
** -limit|n LIMIT The maximum number of items to show
241485
**
242486
** -tkt HASH Filter for only those events for the specified ticket
243487
**
@@ -257,24 +501,28 @@
257501
void cmd_timeline_rss(void){
258502
Stmt q;
259503
int nLine=0;
260504
char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
261505
Blob bSQL;
506
+ Blob base = BLOB_INITIALIZER;
507
+ Blob top = BLOB_INITIALIZER;
262508
const char *zType = find_option("type","y",1); /* Type of events;All if NULL*/
263509
const char *zTicketUuid = find_option("tkt",NULL,1);
264510
const char *zTag = find_option("tag",NULL,1);
265511
const char *zFilename = find_option("name",NULL,1);
266512
const char *zWiki = find_option("wiki",NULL,1);
267513
const char *zLimit = find_option("limit", "n",1);
268514
const char *zBaseURL = find_option("url", NULL, 1);
269515
int nLimit = atoi( (zLimit && *zLimit) ? zLimit : "20" );
270516
int nTagId;
517
+ int bHasForum;
271518
const char zSQL1[] =
272519
@ SELECT
273520
@ blob.rid,
274521
@ uuid,
275522
@ event.mtime,
523
+ @ event.type,
276524
@ coalesce(ecomment,comment),
277525
@ coalesce(euser,user),
278526
@ (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
279527
@ (SELECT count(*) FROM plink WHERE cid=blob.rid),
280528
@ (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
@@ -295,10 +543,21 @@
295543
/* We should be done with options.. */
296544
verify_all_options();
297545
298546
blob_zero(&bSQL);
299547
blob_append( &bSQL, zSQL1, -1 );
548
+ bHasForum = db_table_exists("repository","forumpost");
549
+ if( bHasForum ){
550
+ blob_append_sql(&bSQL,
551
+ " AND (event.type!='f' OR event.objid IN ("
552
+ "SELECT fpid FROM forumpost AS f "
553
+ "WHERE NOT EXISTS(SELECT 1 FROM forumpost AS nx "
554
+ "WHERE nx.fprev=f.fpid)))"
555
+ );
556
+ }else{
557
+ blob_append_sql(&bSQL, " AND event.type!='f'");
558
+ }
300559
301560
if( zType[0]!='a' ){
302561
blob_append_sql(&bSQL, " AND event.type=%Q", zType);
303562
}
304563
@@ -350,14 +609,17 @@
350609
if( zProjectDescr==0 ){
351610
zProjectDescr = zProjectName;
352611
}
353612
354613
zPubDate = cgi_rfc822_datestamp(time(NULL));
614
+ blob_append(&base, zBaseURL, -1);
615
+ rss_top_from_base(&top, zBaseURL);
355616
356617
fossil_print("<?xml version=\"1.0\"?>");
357618
fossil_print("<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
358
- " version=\"2.0\">");
619
+ " xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" "
620
+ " version=\"2.0\">\n");
359621
fossil_print("<channel>\n");
360622
fossil_print("<title>%h</title>\n", zProjectName);
361623
fossil_print("<link>%s</link>\n", zBaseURL);
362624
fossil_print("<description>%h</description>\n", zProjectDescr);
363625
fossil_print("<pubDate>%s</pubDate>\n", zPubDate);
@@ -365,53 +627,98 @@
365627
MANIFEST_VERSION, MANIFEST_DATE);
366628
free(zPubDate);
367629
db_prepare(&q, "%s", blob_sql_text(&bSQL));
368630
blob_reset( &bSQL );
369631
while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
632
+ int rid = db_column_int(&q, 0);
370633
const char *zId = db_column_text(&q, 1);
371
- const char *zCom = db_column_text(&q, 3);
372
- const char *zAuthor = db_column_text(&q, 4);
634
+ const char *zEType = db_column_text(&q, 3);
635
+ const char *zCom = db_column_text(&q, 4);
636
+ const char *zAuthor = db_column_text(&q, 5);
373637
char *zPrefix = "";
374638
char *zSuffix = 0;
375639
char *zDate;
376
- int nChild = db_column_int(&q, 5);
377
- int nParent = db_column_int(&q, 6);
378
- const char *zTagList = db_column_text(&q, 7);
640
+ int nChild = db_column_int(&q, 6);
641
+ int nParent = db_column_int(&q, 7);
642
+ const char *zTagList = db_column_text(&q, 8);
643
+ Manifest *pPost = 0;
644
+ Blob contentHtml = BLOB_INITIALIZER;
645
+ int bForumContent = 0;
379646
time_t ts;
380647
381648
if( zTagList && zTagList[0]==0 ) zTagList = 0;
382649
ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
383650
zDate = cgi_rfc822_datestamp(ts);
384651
385
- if( nParent>1 && nChild>1 ){
386
- zPrefix = "*MERGE/FORK* ";
387
- }else if( nParent>1 ){
388
- zPrefix = "*MERGE* ";
389
- }else if( nChild>1 ){
390
- zPrefix = "*FORK* ";
652
+ if( zEType[0]=='c' ){
653
+ if( nParent>1 && nChild>1 ){
654
+ zPrefix = "*MERGE/FORK* ";
655
+ }else if( nParent>1 ){
656
+ zPrefix = "*MERGE* ";
657
+ }else if( nChild>1 ){
658
+ zPrefix = "*FORK* ";
659
+ }
660
+ }else if( zEType[0]=='w' ){
661
+ switch(zCom ? zCom[0] : 0){
662
+ case ':': zPrefix = "Edit wiki page: "; break;
663
+ case '+': zPrefix = "Add wiki page: "; break;
664
+ case '-': zPrefix = "Delete wiki page: "; break;
665
+ }
666
+ if(*zPrefix) ++zCom;
391667
}
392668
393669
if( zTagList ){
394670
zSuffix = mprintf(" (tags: %s)", zTagList);
395671
}
396672
673
+ if( zEType[0]=='f' ){
674
+ pPost = manifest_get(rid, CFTYPE_FORUM, 0);
675
+ if( pPost ){
676
+ forum_render_to_html(&contentHtml, pPost->zMimetype, pPost->zWiki);
677
+ if( blob_size(&contentHtml)>0 ){
678
+ Blob normalized = BLOB_INITIALIZER;
679
+ rss_make_abs_links(&normalized, blob_str(&base),
680
+ blob_str(&top), blob_str(&contentHtml),
681
+ blob_size(&contentHtml));
682
+ blob_reset(&contentHtml);
683
+ blob_append(&contentHtml, blob_str(&normalized),
684
+ blob_size(&normalized));
685
+ blob_reset(&normalized);
686
+ bForumContent = 1;
687
+ }
688
+ }
689
+ }
397690
fossil_print("<item>");
398691
fossil_print("<title>%s%h%h</title>\n", zPrefix, zCom, zSuffix);
399692
fossil_print("<link>%s/info/%s</link>\n", zBaseURL, zId);
400693
fossil_print("<description>%s%h%h</description>\n", zPrefix, zCom, zSuffix);
401694
fossil_print("<pubDate>%s</pubDate>\n", zDate);
402695
fossil_print("<dc:creator>%h</dc:creator>\n", zAuthor);
403696
fossil_print("<guid>%s/info/%s</guid>\n", g.zBaseURL, zId);
697
+ if( bForumContent ){
698
+ Blob cdata = BLOB_INITIALIZER;
699
+ fossil_print("<dc:format>text/html</dc:format>\n");
700
+ fossil_print("<content:encoded><![CDATA[");
701
+ rss_cdata_append(&cdata, blob_str(&contentHtml),
702
+ blob_size(&contentHtml));
703
+ fossil_print("%s", blob_str(&cdata));
704
+ blob_reset(&cdata);
705
+ fossil_print("]]></content:encoded>\n");
706
+ }
404707
fossil_print("</item>\n");
708
+ if( pPost ) manifest_destroy(pPost);
709
+ blob_reset(&contentHtml);
405710
free(zDate);
406711
free(zSuffix);
407712
nLine++;
408713
}
409714
410715
db_finalize(&q);
716
+ blob_reset(&base);
717
+ blob_reset(&top);
411718
fossil_print("</channel>\n");
412719
fossil_print("</rss>\n");
413720
414721
if( zFreeProjectName != 0 ){
415722
free( zFreeProjectName );
416723
}
417724
}
418725
--- src/rss.c
+++ src/rss.c
@@ -19,10 +19,195 @@
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 **
@@ -44,17 +229,20 @@
44 void page_timeline_rss(void){
45 Stmt q;
46 int nLine=0;
47 char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
48 Blob bSQL;
 
 
49 const char *zType = PD("y","all"); /* Type of events. All if NULL */
50 const char *zTicketUuid = PD("tkt",NULL);
51 const char *zTag = PD("tag",NULL);
52 const char *zFilename = PD("name",NULL);
53 const char *zWiki = PD("wiki",NULL);
54 int nLimit = atoi(PD("n","20"));
55 int nTagId;
 
56 const char zSQL1[] =
57 @ SELECT
58 @ blob.rid,
59 @ uuid,
60 @ event.mtime,
@@ -69,16 +257,27 @@
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_sql( &bSQL, "%s", zSQL1/*safe-for-%s*/ );
 
 
 
 
 
 
 
 
 
 
80
81 if( zType[0]!='a' ){
82 if( zType[0]=='c' && !g.perm.Read ) zType = "x";
83 else if( (zType[0]=='w' || zType[0]=='e') && !g.perm.RdWiki ) zType = "x";
84 else if( zType[0]=='t' && !g.perm.RdTkt ) zType = "x";
@@ -151,13 +350,17 @@
151 if( zProjectDescr==0 ){
152 zProjectDescr = zProjectName;
153 }
154
155 zPubDate = cgi_rfc822_datestamp(time(NULL));
 
 
156
157 @ <?xml version="1.0"?>
158 @ <rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
 
 
159 @ <channel>
160 @ <title>%h(zProjectName)</title>
161 @ <link>%s(g.zBaseURL)</link>
162 @ <description>%h(zProjectDescr)</description>
163 @ <pubDate>%s(zPubDate)</pubDate>
@@ -164,35 +367,39 @@
164 @ <generator>Fossil version %s(MANIFEST_VERSION) %s(MANIFEST_DATE)</generator>
165 free(zPubDate);
166 db_prepare(&q, "%s", blob_sql_text(&bSQL));
167 blob_reset( &bSQL );
168 while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
 
169 const char *zId = db_column_text(&q, 1);
170 const char *zEType = db_column_text(&q, 3);
171 const char *zCom = db_column_text(&q, 4);
172 const char *zAuthor = db_column_text(&q, 5);
173 char *zPrefix = "";
174 char *zSuffix = 0;
175 char *zDate;
176 int nChild = db_column_int(&q, 5);
177 int nParent = db_column_int(&q, 7);
178 const char *zTagList = db_column_text(&q, 8);
 
 
 
179 time_t ts;
180
181 if( zTagList && zTagList[0]==0 ) zTagList = 0;
182 ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
183 zDate = cgi_rfc822_datestamp(ts);
184
185 if('c'==zEType[0]){
186 if( nParent>1 && nChild>1 ){
187 zPrefix = "*MERGE/FORK* ";
188 }else if( nParent>1 ){
189 zPrefix = "*MERGE* ";
190 }else if( nChild>1 ){
191 zPrefix = "*FORK* ";
192 }
193 }else if('w'==zEType[0]){
194 switch(zCom ? zCom[0] : 0){
195 case ':': zPrefix = "Edit wiki page: "; break;
196 case '+': zPrefix = "Add wiki page: "; break;
197 case '-': zPrefix = "Delete wiki page: "; break;
198 }
@@ -201,24 +408,60 @@
201
202 if( zTagList ){
203 zSuffix = mprintf(" (tags: %s)", zTagList);
204 }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206 @ <item>
207 @ <title>%s(zPrefix)%h(zCom)%h(zSuffix)</title>
208 @ <link>%s(g.zBaseURL)/info/%s(zId)</link>
209 @ <description>%s(zPrefix)%h(zCom)%h(zSuffix)</description>
210 @ <pubDate>%s(zDate)</pubDate>
211 @ <dc:creator>%h(zAuthor)</dc:creator>
212 @ <guid>%s(g.zBaseURL)/info/%s(zId)</guid>
 
 
 
 
 
 
 
 
 
 
213 @ </item>
 
 
214 free(zDate);
215 free(zSuffix);
216 nLine++;
217 }
218
219 db_finalize(&q);
 
 
220 @ </channel>
221 @ </rss>
222
223 if( zFreeProjectName != 0 ){
224 free( zFreeProjectName );
@@ -233,11 +476,12 @@
233 ** The CLI variant of the /timeline.rss page, this produces an RSS
234 ** feed of the timeline to stdout.
235 **
236 ** Options:
237 ** -type|y FLAG May be: all (default), ci (show check-ins only),
238 ** t (show tickets only), w (show wiki only)
 
239 **
240 ** -limit|n LIMIT The maximum number of items to show
241 **
242 ** -tkt HASH Filter for only those events for the specified ticket
243 **
@@ -257,24 +501,28 @@
257 void cmd_timeline_rss(void){
258 Stmt q;
259 int nLine=0;
260 char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
261 Blob bSQL;
 
 
262 const char *zType = find_option("type","y",1); /* Type of events;All if NULL*/
263 const char *zTicketUuid = find_option("tkt",NULL,1);
264 const char *zTag = find_option("tag",NULL,1);
265 const char *zFilename = find_option("name",NULL,1);
266 const char *zWiki = find_option("wiki",NULL,1);
267 const char *zLimit = find_option("limit", "n",1);
268 const char *zBaseURL = find_option("url", NULL, 1);
269 int nLimit = atoi( (zLimit && *zLimit) ? zLimit : "20" );
270 int nTagId;
 
271 const char zSQL1[] =
272 @ SELECT
273 @ blob.rid,
274 @ uuid,
275 @ event.mtime,
 
276 @ coalesce(ecomment,comment),
277 @ coalesce(euser,user),
278 @ (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
279 @ (SELECT count(*) FROM plink WHERE cid=blob.rid),
280 @ (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
@@ -295,10 +543,21 @@
295 /* We should be done with options.. */
296 verify_all_options();
297
298 blob_zero(&bSQL);
299 blob_append( &bSQL, zSQL1, -1 );
 
 
 
 
 
 
 
 
 
 
 
300
301 if( zType[0]!='a' ){
302 blob_append_sql(&bSQL, " AND event.type=%Q", zType);
303 }
304
@@ -350,14 +609,17 @@
350 if( zProjectDescr==0 ){
351 zProjectDescr = zProjectName;
352 }
353
354 zPubDate = cgi_rfc822_datestamp(time(NULL));
 
 
355
356 fossil_print("<?xml version=\"1.0\"?>");
357 fossil_print("<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
358 " version=\"2.0\">");
 
359 fossil_print("<channel>\n");
360 fossil_print("<title>%h</title>\n", zProjectName);
361 fossil_print("<link>%s</link>\n", zBaseURL);
362 fossil_print("<description>%h</description>\n", zProjectDescr);
363 fossil_print("<pubDate>%s</pubDate>\n", zPubDate);
@@ -365,53 +627,98 @@
365 MANIFEST_VERSION, MANIFEST_DATE);
366 free(zPubDate);
367 db_prepare(&q, "%s", blob_sql_text(&bSQL));
368 blob_reset( &bSQL );
369 while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
 
370 const char *zId = db_column_text(&q, 1);
371 const char *zCom = db_column_text(&q, 3);
372 const char *zAuthor = db_column_text(&q, 4);
 
373 char *zPrefix = "";
374 char *zSuffix = 0;
375 char *zDate;
376 int nChild = db_column_int(&q, 5);
377 int nParent = db_column_int(&q, 6);
378 const char *zTagList = db_column_text(&q, 7);
 
 
 
379 time_t ts;
380
381 if( zTagList && zTagList[0]==0 ) zTagList = 0;
382 ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
383 zDate = cgi_rfc822_datestamp(ts);
384
385 if( nParent>1 && nChild>1 ){
386 zPrefix = "*MERGE/FORK* ";
387 }else if( nParent>1 ){
388 zPrefix = "*MERGE* ";
389 }else if( nChild>1 ){
390 zPrefix = "*FORK* ";
 
 
 
 
 
 
 
 
 
391 }
392
393 if( zTagList ){
394 zSuffix = mprintf(" (tags: %s)", zTagList);
395 }
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397 fossil_print("<item>");
398 fossil_print("<title>%s%h%h</title>\n", zPrefix, zCom, zSuffix);
399 fossil_print("<link>%s/info/%s</link>\n", zBaseURL, zId);
400 fossil_print("<description>%s%h%h</description>\n", zPrefix, zCom, zSuffix);
401 fossil_print("<pubDate>%s</pubDate>\n", zDate);
402 fossil_print("<dc:creator>%h</dc:creator>\n", zAuthor);
403 fossil_print("<guid>%s/info/%s</guid>\n", g.zBaseURL, zId);
 
 
 
 
 
 
 
 
 
 
404 fossil_print("</item>\n");
 
 
405 free(zDate);
406 free(zSuffix);
407 nLine++;
408 }
409
410 db_finalize(&q);
 
 
411 fossil_print("</channel>\n");
412 fossil_print("</rss>\n");
413
414 if( zFreeProjectName != 0 ){
415 free( zFreeProjectName );
416 }
417 }
418
--- src/rss.c
+++ src/rss.c
@@ -19,10 +19,195 @@
19 */
20 #include "config.h"
21 #include <time.h>
22 #include "rss.h"
23 #include <assert.h>
24
25 void forum_render_to_html(struct Blob*, const char*, const char*);
26
27 /*
28 ** Append text to pOut, escaping any CDATA terminators.
29 */
30 static void rss_cdata_append(Blob *pOut, const char *zIn, int nIn){
31 const char *zEnd;
32 const char *zDelim;
33 if( pOut==0 ) return;
34 if( zIn==0 ) zIn = "";
35 if( nIn<0 ) nIn = (int)strlen(zIn);
36 zEnd = zIn + nIn;
37 while( zIn<zEnd && (zDelim = strstr(zIn, "]]>") )!=0 ){
38 if( zDelim>=zEnd ) break;
39 blob_append(pOut, zIn, (int)(zDelim - zIn));
40 blob_append_literal(pOut, "]]]]><![CDATA[>");
41 zIn = zDelim + 3;
42 }
43 if( zIn<zEnd ){
44 blob_append(pOut, zIn, (int)(zEnd - zIn));
45 }
46 }
47
48 /*
49 ** Return true if zIn looks like an absolute URL.
50 */
51 static int rss_is_absolute_url(const char *zIn, int nIn){
52 int i;
53 if( zIn==0 || nIn<=0 ) return 0;
54 if( nIn>=2 && zIn[0]=='/' && zIn[1]=='/' ) return 1;
55 if( !((zIn[0]>='a' && zIn[0]<='z') || (zIn[0]>='A' && zIn[0]<='Z')) ){
56 return 0;
57 }
58 for(i=1; i<nIn; i++){
59 char c = zIn[i];
60 if( (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9')
61 || c=='+' || c=='-' || c=='.' ){
62 continue;
63 }
64 return c==':' ? 1 : 0;
65 }
66 return 0;
67 }
68
69 /*
70 ** Return the length of zBase without trailing slashes.
71 */
72 static int rss_trim_base(const char *zBase){
73 int n = zBase ? (int)strlen(zBase) : 0;
74 while( n>0 && zBase[n-1]=='/' ) n--;
75 return n;
76 }
77
78 /*
79 ** Compute the repository top path from zBase and return it.
80 */
81 static const char *rss_top_from_base(Blob *pTop, const char *zBase){
82 const char *z = zBase;
83 const char *zSlash = 0;
84 int n;
85 if( zBase==0 ) return "";
86 if( strncmp(zBase, "http://", 7)==0 ){
87 z = zBase + 7;
88 }else if( strncmp(zBase, "https://", 8)==0 ){
89 z = zBase + 8;
90 }else{
91 return "";
92 }
93 zSlash = strchr(z, '/');
94 if( zSlash==0 ) return "";
95 n = (int)strlen(zSlash);
96 while( n>1 && zSlash[n-1]=='/' ) n--;
97 blob_init(pTop, zSlash, n);
98 return blob_str(pTop);
99 }
100
101 /*
102 ** Append an absolute URL to pOut, using zBase/zTop as the base.
103 */
104 static void rss_append_abs_url(
105 Blob *pOut,
106 const char *zBase,
107 int nBase,
108 const char *zTop,
109 int nTop,
110 const char *zRel,
111 int nRel
112 ){
113 if( pOut==0 ) return;
114 if( zRel==0 || nRel<=0 ) return;
115 if( zBase==0 || zBase[0]==0 ){
116 blob_append(pOut, zRel, nRel);
117 return;
118 }
119 if( zRel[0]=='#' || rss_is_absolute_url(zRel, nRel) ){
120 blob_append(pOut, zRel, nRel);
121 return;
122 }
123 if( zRel[0]=='/' ){
124 if( nTop>1 && strncmp(zRel, zTop, nTop)==0 ){
125 blob_append(pOut, zBase, nBase);
126 blob_append(pOut, zRel + nTop, nRel - nTop);
127 }else{
128 blob_append(pOut, zBase, nBase);
129 blob_append(pOut, zRel, nRel);
130 }
131 }else{
132 blob_append(pOut, zBase, nBase);
133 blob_append_char(pOut, '/');
134 blob_append(pOut, zRel, nRel);
135 }
136 }
137
138 /*
139 ** Convert relative href/src attributes in zIn to absolute URLs.
140 */
141 static void rss_make_abs_links(
142 Blob *pOut,
143 const char *zBase,
144 const char *zTop,
145 const char *zIn,
146 int nIn
147 ){
148 const char *z = zIn;
149 const char *zEnd = zIn + nIn;
150 const char *zLast = zIn;
151 int nBase = rss_trim_base(zBase);
152 int nTop = zTop ? (int)strlen(zTop) : 0;
153 if( pOut==0 || zIn==0 ) return;
154 while( z<zEnd ){
155 int nAttr = 0;
156 if( (z>zIn && !(z[-1]==' ' || (z[-1]>='\t' && z[-1]<='\r'))
157 && z[-1]!='<') ){
158 z++;
159 continue;
160 }
161 if( zEnd - z >= 5
162 && (z[0]=='h' || z[0]=='H')
163 && (z[1]=='r' || z[1]=='R')
164 && (z[2]=='e' || z[2]=='E')
165 && (z[3]=='f' || z[3]=='F')
166 && z[4]=='='
167 ){
168 nAttr = 5;
169 }else if( zEnd - z >= 4
170 && (z[0]=='s' || z[0]=='S')
171 && (z[1]=='r' || z[1]=='R')
172 && (z[2]=='c' || z[2]=='C')
173 && z[3]=='='
174 ){
175 nAttr = 4;
176 }
177 if( nAttr==0 ){
178 z++;
179 continue;
180 }
181 {
182 const char *zVal = z + nAttr;
183 const char *zValEnd = zVal;
184 char quote = 0;
185 if( zVal>=zEnd ) break;
186 if( *zVal=='"' || *zVal=='\'' ){
187 quote = *zVal;
188 zVal++;
189 }
190 zValEnd = zVal;
191 while( zValEnd<zEnd ){
192 if( quote ){
193 if( *zValEnd==quote ) break;
194 }else if( *zValEnd==' ' || (*zValEnd>='\t' && *zValEnd<='\r')
195 || *zValEnd=='>' ){
196 break;
197 }
198 zValEnd++;
199 }
200 blob_append(pOut, zLast, zVal - zLast);
201 rss_append_abs_url(pOut, zBase, nBase, zTop, nTop,
202 zVal, (int)(zValEnd - zVal));
203 zLast = zValEnd;
204 z = zValEnd;
205 }
206 }
207 if( zLast<zEnd ) blob_append(pOut, zLast, (int)(zEnd - zLast));
208 }
209
210 /*
211 ** WEBPAGE: timeline.rss
212 ** URL: /timeline.rss?y=TYPE&n=LIMIT&tkt=HASH&tag=TAG&wiki=NAME&name=FILENAME
213 **
@@ -44,17 +229,20 @@
229 void page_timeline_rss(void){
230 Stmt q;
231 int nLine=0;
232 char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
233 Blob bSQL;
234 Blob base = BLOB_INITIALIZER;
235 Blob top = BLOB_INITIALIZER;
236 const char *zType = PD("y","all"); /* Type of events. All if NULL */
237 const char *zTicketUuid = PD("tkt",NULL);
238 const char *zTag = PD("tag",NULL);
239 const char *zFilename = PD("name",NULL);
240 const char *zWiki = PD("wiki",NULL);
241 int nLimit = atoi(PD("n","20"));
242 int nTagId;
243 int bHasForum;
244 const char zSQL1[] =
245 @ SELECT
246 @ blob.rid,
247 @ uuid,
248 @ event.mtime,
@@ -69,16 +257,27 @@
257 @ FROM event, blob
258 @ WHERE blob.rid=event.objid
259 ;
260
261 login_check_credentials();
262 if( !g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum ){
263 return;
264 }
265 bHasForum = db_table_exists("repository","forumpost");
266
267 blob_zero(&bSQL);
268 blob_append_sql( &bSQL, "%s", zSQL1/*safe-for-%s*/ );
269 if( bHasForum ){
270 blob_append_sql(&bSQL,
271 " AND (event.type!='f' OR event.objid IN ("
272 "SELECT fpid FROM forumpost AS f "
273 "WHERE NOT EXISTS(SELECT 1 FROM forumpost AS nx "
274 "WHERE nx.fprev=f.fpid)))"
275 );
276 }else{
277 blob_append_sql(&bSQL, " AND event.type!='f'");
278 }
279
280 if( zType[0]!='a' ){
281 if( zType[0]=='c' && !g.perm.Read ) zType = "x";
282 else if( (zType[0]=='w' || zType[0]=='e') && !g.perm.RdWiki ) zType = "x";
283 else if( zType[0]=='t' && !g.perm.RdTkt ) zType = "x";
@@ -151,13 +350,17 @@
350 if( zProjectDescr==0 ){
351 zProjectDescr = zProjectName;
352 }
353
354 zPubDate = cgi_rfc822_datestamp(time(NULL));
355 blob_append(&base, g.zBaseURL, -1);
356 rss_top_from_base(&top, g.zBaseURL);
357
358 @ <?xml version="1.0"?>
359 @ <rss xmlns:dc="http://purl.org/dc/elements/1.1/" \
360 @ xmlns:content="http://purl.org/rss/1.0/modules/content/" \
361 @ version="2.0">
362 @ <channel>
363 @ <title>%h(zProjectName)</title>
364 @ <link>%s(g.zBaseURL)</link>
365 @ <description>%h(zProjectDescr)</description>
366 @ <pubDate>%s(zPubDate)</pubDate>
@@ -164,35 +367,39 @@
367 @ <generator>Fossil version %s(MANIFEST_VERSION) %s(MANIFEST_DATE)</generator>
368 free(zPubDate);
369 db_prepare(&q, "%s", blob_sql_text(&bSQL));
370 blob_reset( &bSQL );
371 while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
372 int rid = db_column_int(&q, 0);
373 const char *zId = db_column_text(&q, 1);
374 const char *zEType = db_column_text(&q, 3);
375 const char *zCom = db_column_text(&q, 4);
376 const char *zAuthor = db_column_text(&q, 5);
377 char *zPrefix = "";
378 char *zSuffix = 0;
379 char *zDate;
380 int nChild = db_column_int(&q, 6);
381 int nParent = db_column_int(&q, 7);
382 const char *zTagList = db_column_text(&q, 8);
383 Manifest *pPost = 0;
384 Blob contentHtml = BLOB_INITIALIZER;
385 int bForumContent = 0;
386 time_t ts;
387
388 if( zTagList && zTagList[0]==0 ) zTagList = 0;
389 ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
390 zDate = cgi_rfc822_datestamp(ts);
391
392 if( zEType[0]=='c' ){
393 if( nParent>1 && nChild>1 ){
394 zPrefix = "*MERGE/FORK* ";
395 }else if( nParent>1 ){
396 zPrefix = "*MERGE* ";
397 }else if( nChild>1 ){
398 zPrefix = "*FORK* ";
399 }
400 }else if( zEType[0]=='w' ){
401 switch(zCom ? zCom[0] : 0){
402 case ':': zPrefix = "Edit wiki page: "; break;
403 case '+': zPrefix = "Add wiki page: "; break;
404 case '-': zPrefix = "Delete wiki page: "; break;
405 }
@@ -201,24 +408,60 @@
408
409 if( zTagList ){
410 zSuffix = mprintf(" (tags: %s)", zTagList);
411 }
412
413 if( zEType[0]=='f' ){
414 if( !g.perm.ModForum && content_is_private(rid) ){
415 free(zDate);
416 free(zSuffix);
417 continue;
418 }
419 pPost = manifest_get(rid, CFTYPE_FORUM, 0);
420 if( pPost ){
421 forum_render_to_html(&contentHtml, pPost->zMimetype, pPost->zWiki);
422 if( blob_size(&contentHtml)>0 ){
423 Blob normalized = BLOB_INITIALIZER;
424 rss_make_abs_links(&normalized, blob_str(&base),
425 blob_str(&top), blob_str(&contentHtml),
426 blob_size(&contentHtml));
427 blob_reset(&contentHtml);
428 blob_append(&contentHtml, blob_str(&normalized),
429 blob_size(&normalized));
430 blob_reset(&normalized);
431 bForumContent = 1;
432 }
433 }
434 }
435 @ <item>
436 @ <title>%s(zPrefix)%h(zCom)%h(zSuffix)</title>
437 @ <link>%s(g.zBaseURL)/info/%s(zId)</link>
438 @ <description>%s(zPrefix)%h(zCom)%h(zSuffix)</description>
439 @ <pubDate>%s(zDate)</pubDate>
440 @ <dc:creator>%h(zAuthor)</dc:creator>
441 @ <guid>%s(g.zBaseURL)/info/%s(zId)</guid>
442 if( bForumContent ){
443 Blob cdata = BLOB_INITIALIZER;
444 @ <dc:format>text/html</dc:format>
445 @ <content:encoded><![CDATA[
446 rss_cdata_append(&cdata, blob_str(&contentHtml),
447 blob_size(&contentHtml));
448 cgi_append_content(blob_str(&cdata), blob_size(&cdata));
449 blob_reset(&cdata);
450 @ ]]></content:encoded>
451 }
452 @ </item>
453 if( pPost ) manifest_destroy(pPost);
454 blob_reset(&contentHtml);
455 free(zDate);
456 free(zSuffix);
457 nLine++;
458 }
459
460 db_finalize(&q);
461 blob_reset(&base);
462 blob_reset(&top);
463 @ </channel>
464 @ </rss>
465
466 if( zFreeProjectName != 0 ){
467 free( zFreeProjectName );
@@ -233,11 +476,12 @@
476 ** The CLI variant of the /timeline.rss page, this produces an RSS
477 ** feed of the timeline to stdout.
478 **
479 ** Options:
480 ** -type|y FLAG May be: all (default), ci (show check-ins only),
481 ** t (show tickets only), w (show wiki only),
482 ** e (show tech notes only), f (show forum posts only)
483 **
484 ** -limit|n LIMIT The maximum number of items to show
485 **
486 ** -tkt HASH Filter for only those events for the specified ticket
487 **
@@ -257,24 +501,28 @@
501 void cmd_timeline_rss(void){
502 Stmt q;
503 int nLine=0;
504 char *zPubDate, *zProjectName, *zProjectDescr, *zFreeProjectName=0;
505 Blob bSQL;
506 Blob base = BLOB_INITIALIZER;
507 Blob top = BLOB_INITIALIZER;
508 const char *zType = find_option("type","y",1); /* Type of events;All if NULL*/
509 const char *zTicketUuid = find_option("tkt",NULL,1);
510 const char *zTag = find_option("tag",NULL,1);
511 const char *zFilename = find_option("name",NULL,1);
512 const char *zWiki = find_option("wiki",NULL,1);
513 const char *zLimit = find_option("limit", "n",1);
514 const char *zBaseURL = find_option("url", NULL, 1);
515 int nLimit = atoi( (zLimit && *zLimit) ? zLimit : "20" );
516 int nTagId;
517 int bHasForum;
518 const char zSQL1[] =
519 @ SELECT
520 @ blob.rid,
521 @ uuid,
522 @ event.mtime,
523 @ event.type,
524 @ coalesce(ecomment,comment),
525 @ coalesce(euser,user),
526 @ (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
527 @ (SELECT count(*) FROM plink WHERE cid=blob.rid),
528 @ (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
@@ -295,10 +543,21 @@
543 /* We should be done with options.. */
544 verify_all_options();
545
546 blob_zero(&bSQL);
547 blob_append( &bSQL, zSQL1, -1 );
548 bHasForum = db_table_exists("repository","forumpost");
549 if( bHasForum ){
550 blob_append_sql(&bSQL,
551 " AND (event.type!='f' OR event.objid IN ("
552 "SELECT fpid FROM forumpost AS f "
553 "WHERE NOT EXISTS(SELECT 1 FROM forumpost AS nx "
554 "WHERE nx.fprev=f.fpid)))"
555 );
556 }else{
557 blob_append_sql(&bSQL, " AND event.type!='f'");
558 }
559
560 if( zType[0]!='a' ){
561 blob_append_sql(&bSQL, " AND event.type=%Q", zType);
562 }
563
@@ -350,14 +609,17 @@
609 if( zProjectDescr==0 ){
610 zProjectDescr = zProjectName;
611 }
612
613 zPubDate = cgi_rfc822_datestamp(time(NULL));
614 blob_append(&base, zBaseURL, -1);
615 rss_top_from_base(&top, zBaseURL);
616
617 fossil_print("<?xml version=\"1.0\"?>");
618 fossil_print("<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
619 " xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" "
620 " version=\"2.0\">\n");
621 fossil_print("<channel>\n");
622 fossil_print("<title>%h</title>\n", zProjectName);
623 fossil_print("<link>%s</link>\n", zBaseURL);
624 fossil_print("<description>%h</description>\n", zProjectDescr);
625 fossil_print("<pubDate>%s</pubDate>\n", zPubDate);
@@ -365,53 +627,98 @@
627 MANIFEST_VERSION, MANIFEST_DATE);
628 free(zPubDate);
629 db_prepare(&q, "%s", blob_sql_text(&bSQL));
630 blob_reset( &bSQL );
631 while( db_step(&q)==SQLITE_ROW && nLine<nLimit ){
632 int rid = db_column_int(&q, 0);
633 const char *zId = db_column_text(&q, 1);
634 const char *zEType = db_column_text(&q, 3);
635 const char *zCom = db_column_text(&q, 4);
636 const char *zAuthor = db_column_text(&q, 5);
637 char *zPrefix = "";
638 char *zSuffix = 0;
639 char *zDate;
640 int nChild = db_column_int(&q, 6);
641 int nParent = db_column_int(&q, 7);
642 const char *zTagList = db_column_text(&q, 8);
643 Manifest *pPost = 0;
644 Blob contentHtml = BLOB_INITIALIZER;
645 int bForumContent = 0;
646 time_t ts;
647
648 if( zTagList && zTagList[0]==0 ) zTagList = 0;
649 ts = (time_t)((db_column_double(&q,2) - 2440587.5)*86400.0);
650 zDate = cgi_rfc822_datestamp(ts);
651
652 if( zEType[0]=='c' ){
653 if( nParent>1 && nChild>1 ){
654 zPrefix = "*MERGE/FORK* ";
655 }else if( nParent>1 ){
656 zPrefix = "*MERGE* ";
657 }else if( nChild>1 ){
658 zPrefix = "*FORK* ";
659 }
660 }else if( zEType[0]=='w' ){
661 switch(zCom ? zCom[0] : 0){
662 case ':': zPrefix = "Edit wiki page: "; break;
663 case '+': zPrefix = "Add wiki page: "; break;
664 case '-': zPrefix = "Delete wiki page: "; break;
665 }
666 if(*zPrefix) ++zCom;
667 }
668
669 if( zTagList ){
670 zSuffix = mprintf(" (tags: %s)", zTagList);
671 }
672
673 if( zEType[0]=='f' ){
674 pPost = manifest_get(rid, CFTYPE_FORUM, 0);
675 if( pPost ){
676 forum_render_to_html(&contentHtml, pPost->zMimetype, pPost->zWiki);
677 if( blob_size(&contentHtml)>0 ){
678 Blob normalized = BLOB_INITIALIZER;
679 rss_make_abs_links(&normalized, blob_str(&base),
680 blob_str(&top), blob_str(&contentHtml),
681 blob_size(&contentHtml));
682 blob_reset(&contentHtml);
683 blob_append(&contentHtml, blob_str(&normalized),
684 blob_size(&normalized));
685 blob_reset(&normalized);
686 bForumContent = 1;
687 }
688 }
689 }
690 fossil_print("<item>");
691 fossil_print("<title>%s%h%h</title>\n", zPrefix, zCom, zSuffix);
692 fossil_print("<link>%s/info/%s</link>\n", zBaseURL, zId);
693 fossil_print("<description>%s%h%h</description>\n", zPrefix, zCom, zSuffix);
694 fossil_print("<pubDate>%s</pubDate>\n", zDate);
695 fossil_print("<dc:creator>%h</dc:creator>\n", zAuthor);
696 fossil_print("<guid>%s/info/%s</guid>\n", g.zBaseURL, zId);
697 if( bForumContent ){
698 Blob cdata = BLOB_INITIALIZER;
699 fossil_print("<dc:format>text/html</dc:format>\n");
700 fossil_print("<content:encoded><![CDATA[");
701 rss_cdata_append(&cdata, blob_str(&contentHtml),
702 blob_size(&contentHtml));
703 fossil_print("%s", blob_str(&cdata));
704 blob_reset(&cdata);
705 fossil_print("]]></content:encoded>\n");
706 }
707 fossil_print("</item>\n");
708 if( pPost ) manifest_destroy(pPost);
709 blob_reset(&contentHtml);
710 free(zDate);
711 free(zSuffix);
712 nLine++;
713 }
714
715 db_finalize(&q);
716 blob_reset(&base);
717 blob_reset(&top);
718 fossil_print("</channel>\n");
719 fossil_print("</rss>\n");
720
721 if( zFreeProjectName != 0 ){
722 free( zFreeProjectName );
723 }
724 }
725
+59
--- src/wiki.c
+++ src/wiki.c
@@ -2556,10 +2556,18 @@
25562556
#if INTERFACE
25572557
#define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
25582558
#define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
25592559
#define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
25602560
#define WIKIASSOC_ALL 0x00007 /* All of the above */
2561
+
2562
+/* Render wiki/markdown/plaintext to HTML in an output blob. */
2563
+void wiki_convert_to_html(
2564
+ Blob *pOut,
2565
+ const char *zMimetype,
2566
+ const char *zContent,
2567
+ int eDocSrc
2568
+);
25612569
#endif
25622570
25632571
/*
25642572
** Show the default Section label for an associated wiki page.
25652573
*/
@@ -2603,10 +2611,61 @@
26032611
){
26042612
if( g.perm.WrWiki && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
26052613
style_submenu_element("Edit Wiki", "%R/wikiedit?name=%s/%t", zPrefix, zName);
26062614
}
26072615
}
2616
+
2617
+/*
2618
+** Render wiki/markdown/plaintext content into an output blob as HTML.
2619
+*/
2620
+void wiki_convert_to_html(
2621
+ Blob *pOut,
2622
+ const char *zMimetype,
2623
+ const char *zContent,
2624
+ int eDocSrc
2625
+){
2626
+ Blob in;
2627
+ if( pOut==0 ) return;
2628
+ if( zContent==0 || zContent[0]==0 ){
2629
+ blob_append_literal(pOut, "<i>Deleted</i>");
2630
+ return;
2631
+ }
2632
+ blob_init(&in, 0, 0);
2633
+ blob_append(&in, zContent, -1);
2634
+ safe_html_context(eDocSrc);
2635
+ if( zMimetype==0 || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
2636
+ wiki_convert(&in, pOut, 0);
2637
+ }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
2638
+ markdown_to_html(&in, 0, pOut);
2639
+ safe_html(pOut);
2640
+ }else if( fossil_strcmp(zMimetype, "text/x-pikchr")==0 ){
2641
+ const char *zPikchr = blob_str(&in);
2642
+ int w = 0;
2643
+ int h = 0;
2644
+ char *zOut = pikchr(zPikchr, "pikchr", 0, &w, &h);
2645
+ if( w>0 ){
2646
+ blob_appendf(pOut,
2647
+ "<div class=\"pikchr-svg\" style=\"max-width:%dpx\">", w);
2648
+ blob_append(pOut, zOut, -1);
2649
+ blob_append_literal(pOut, "</div>");
2650
+ }else{
2651
+ blob_append_literal(pOut, "<pre class='error'>");
2652
+ htmlize_to_blob(pOut, zOut, -1);
2653
+ blob_append_literal(pOut, "</pre>");
2654
+ }
2655
+ free(zOut);
2656
+ }else if( fossil_strcmp(zMimetype, "text/plain")==0 ){
2657
+ blob_append_literal(pOut, "<pre class='textPlain'>");
2658
+ htmlize_to_blob(pOut, blob_str(&in), blob_size(&in));
2659
+ blob_append_literal(pOut, "</pre>");
2660
+ }else{
2661
+ blob_append_literal(pOut, "<pre class='textPlain'>");
2662
+ htmlize_to_blob(pOut, blob_str(&in), blob_size(&in));
2663
+ blob_append_literal(pOut, "</pre>");
2664
+ }
2665
+ blob_reset(&in);
2666
+}
26082667
26092668
/*
26102669
** Check to see if there exists a wiki page with a name zPrefix/zName.
26112670
** If there is, then render a <div class='section'>..</div> and
26122671
** return true.
26132672
--- src/wiki.c
+++ src/wiki.c
@@ -2556,10 +2556,18 @@
2556 #if INTERFACE
2557 #define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
2558 #define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
2559 #define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
2560 #define WIKIASSOC_ALL 0x00007 /* All of the above */
 
 
 
 
 
 
 
 
2561 #endif
2562
2563 /*
2564 ** Show the default Section label for an associated wiki page.
2565 */
@@ -2603,10 +2611,61 @@
2603 ){
2604 if( g.perm.WrWiki && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
2605 style_submenu_element("Edit Wiki", "%R/wikiedit?name=%s/%t", zPrefix, zName);
2606 }
2607 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2608
2609 /*
2610 ** Check to see if there exists a wiki page with a name zPrefix/zName.
2611 ** If there is, then render a <div class='section'>..</div> and
2612 ** return true.
2613
--- src/wiki.c
+++ src/wiki.c
@@ -2556,10 +2556,18 @@
2556 #if INTERFACE
2557 #define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
2558 #define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
2559 #define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
2560 #define WIKIASSOC_ALL 0x00007 /* All of the above */
2561
2562 /* Render wiki/markdown/plaintext to HTML in an output blob. */
2563 void wiki_convert_to_html(
2564 Blob *pOut,
2565 const char *zMimetype,
2566 const char *zContent,
2567 int eDocSrc
2568 );
2569 #endif
2570
2571 /*
2572 ** Show the default Section label for an associated wiki page.
2573 */
@@ -2603,10 +2611,61 @@
2611 ){
2612 if( g.perm.WrWiki && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
2613 style_submenu_element("Edit Wiki", "%R/wikiedit?name=%s/%t", zPrefix, zName);
2614 }
2615 }
2616
2617 /*
2618 ** Render wiki/markdown/plaintext content into an output blob as HTML.
2619 */
2620 void wiki_convert_to_html(
2621 Blob *pOut,
2622 const char *zMimetype,
2623 const char *zContent,
2624 int eDocSrc
2625 ){
2626 Blob in;
2627 if( pOut==0 ) return;
2628 if( zContent==0 || zContent[0]==0 ){
2629 blob_append_literal(pOut, "<i>Deleted</i>");
2630 return;
2631 }
2632 blob_init(&in, 0, 0);
2633 blob_append(&in, zContent, -1);
2634 safe_html_context(eDocSrc);
2635 if( zMimetype==0 || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
2636 wiki_convert(&in, pOut, 0);
2637 }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
2638 markdown_to_html(&in, 0, pOut);
2639 safe_html(pOut);
2640 }else if( fossil_strcmp(zMimetype, "text/x-pikchr")==0 ){
2641 const char *zPikchr = blob_str(&in);
2642 int w = 0;
2643 int h = 0;
2644 char *zOut = pikchr(zPikchr, "pikchr", 0, &w, &h);
2645 if( w>0 ){
2646 blob_appendf(pOut,
2647 "<div class=\"pikchr-svg\" style=\"max-width:%dpx\">", w);
2648 blob_append(pOut, zOut, -1);
2649 blob_append_literal(pOut, "</div>");
2650 }else{
2651 blob_append_literal(pOut, "<pre class='error'>");
2652 htmlize_to_blob(pOut, zOut, -1);
2653 blob_append_literal(pOut, "</pre>");
2654 }
2655 free(zOut);
2656 }else if( fossil_strcmp(zMimetype, "text/plain")==0 ){
2657 blob_append_literal(pOut, "<pre class='textPlain'>");
2658 htmlize_to_blob(pOut, blob_str(&in), blob_size(&in));
2659 blob_append_literal(pOut, "</pre>");
2660 }else{
2661 blob_append_literal(pOut, "<pre class='textPlain'>");
2662 htmlize_to_blob(pOut, blob_str(&in), blob_size(&in));
2663 blob_append_literal(pOut, "</pre>");
2664 }
2665 blob_reset(&in);
2666 }
2667
2668 /*
2669 ** Check to see if there exists a wiki page with a name zPrefix/zName.
2670 ** If there is, then render a <div class='section'>..</div> and
2671 ** return true.
2672

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button