Fossil SCM

fossil-scm / src / backlink.c
Blame History Raw 460 lines
1
/*
2
** Copyright (c) 2020 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
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code to implement for managing backlinks and
19
** the "backlink" table of the repository database.
20
**
21
** A backlink is a reference in Fossil-Wiki or Markdown to some other
22
** object in the repository.
23
*/
24
#include "config.h"
25
#include "backlink.h"
26
#include <assert.h>
27
28
29
/*
30
** Show a graph of all wiki, tickets, and check-ins that refer to object zUuid.
31
**
32
** If zLabel is not NULL and the graph is not empty, then output zLabel as
33
** a prefix to the graph.
34
*/
35
void render_backlink_graph(const char *zUuid, const char *zLabel){
36
Blob sql;
37
Stmt q;
38
char *zGlob;
39
int needEndPanel = 0;
40
zGlob = mprintf("%.5s*", zUuid);
41
db_multi_exec(
42
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);\n"
43
"DELETE FROM ok;\n"
44
"INSERT OR IGNORE INTO ok(rid)\n"
45
" SELECT CASE srctype\n"
46
" WHEN 2 THEN (SELECT rid FROM tagxref WHERE tagid=backlink.srcid\n"
47
" ORDER BY mtime DESC LIMIT 1)\n"
48
" ELSE srcid END\n"
49
" FROM backlink\n"
50
" WHERE target GLOB %Q"
51
" AND %Q GLOB (target || '*');",
52
zGlob, zUuid
53
);
54
if( !db_exists("SELECT 1 FROM ok") ) return;
55
if( zLabel ){
56
cgi_printf("%s", zLabel);
57
if( strstr(zLabel, "accordion")!=0 ){
58
cgi_printf("<div class=\"accordion_panel\">\n");
59
needEndPanel = 1;
60
}
61
}
62
blob_zero(&sql);
63
blob_append(&sql, timeline_query_for_www(), -1);
64
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
65
db_prepare(&q, "%s", blob_sql_text(&sql));
66
www_print_timeline(&q,
67
TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL|TIMELINE_REFS, 0);
68
db_finalize(&q);
69
if( needEndPanel ){
70
cgi_printf("</div>\n");
71
}
72
}
73
74
/*
75
** WEBPAGE: test-backlink-timeline
76
**
77
** Show a timeline of all check-ins and other events that have entries
78
** in the backlink table. This is used for testing the rendering
79
** of the "References" section of the /info page.
80
*/
81
void backlink_timeline_page(void){
82
Blob sql;
83
Stmt q;
84
85
login_check_credentials();
86
if( !g.perm.Read || !g.perm.RdTkt || !g.perm.RdWiki ){
87
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
88
return;
89
}
90
style_set_current_feature("test");
91
style_header("Backlink Timeline (Internal Testing Use)");
92
db_multi_exec(
93
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);"
94
"DELETE FROM ok;"
95
"INSERT OR IGNORE INTO ok"
96
" SELECT blob.rid FROM backlink, blob"
97
" WHERE blob.uuid BETWEEN backlink.target AND (backlink.target||'x')"
98
);
99
blob_zero(&sql);
100
blob_append(&sql, timeline_query_for_www(), -1);
101
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
102
db_prepare(&q, "%s", blob_sql_text(&sql));
103
www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL,0);
104
db_finalize(&q);
105
style_finish_page();
106
}
107
108
/*
109
** WEBPAGE: test-backlinks
110
**
111
** Show a table of all backlinks. Admin access only.
112
*/
113
void backlink_table_page(void){
114
Stmt q;
115
int n;
116
login_check_credentials();
117
if( !g.perm.Admin ){
118
login_needed(g.anon.Admin);
119
return;
120
}
121
style_set_current_feature("test");
122
style_header("Backlink Table (Internal Testing Use)");
123
n = db_int(0, "SELECT count(*) FROM backlink");
124
@ <p>%d(n) backlink table entries:</p>
125
db_prepare(&q,
126
"SELECT target, srctype, srcid, datetime(mtime),"
127
" CASE srctype"
128
" WHEN 2 THEN (SELECT substr(tagname,6) FROM tag"
129
" WHERE tagid=srcid AND tagname GLOB 'wiki-*')"
130
" ELSE null END FROM backlink"
131
);
132
style_table_sorter();
133
@ <table border="1" cellpadding="2" cellspacing="0" \
134
@ class='sortable' data-column-types='ttt' data-init-sort='0'>
135
@ <thead><tr><th> Target <th> Source <th> mtime </tr></thead>
136
@ <tbody>
137
while( db_step(&q)==SQLITE_ROW ){
138
const char *zTarget = db_column_text(&q, 0);
139
int srctype = db_column_int(&q, 1);
140
int srcid = db_column_int(&q, 2);
141
const char *zMtime = db_column_text(&q, 3);
142
@ <tr><td><a href="%R/info/%h(zTarget)">%h(zTarget)</a>
143
switch( srctype ){
144
case BKLNK_COMMENT: {
145
@ <td><a href="%R/info?name=rid:%d(srcid)">checkin-%d(srcid)</a>
146
break;
147
}
148
case BKLNK_TICKET: {
149
@ <td><a href="%R/info?name=rid:%d(srcid)">ticket-%d(srcid)</a>
150
break;
151
}
152
case BKLNK_WIKI: {
153
const char *zName = db_column_text(&q, 4);
154
@ <td><a href="%R/wiki?name=%h(zName)&p">wiki-%d(srcid)</a>
155
break;
156
}
157
case BKLNK_EVENT: {
158
@ <td><a href="%R/info?name=rid:%d(srcid)">tecknote-%d(srcid)</a>
159
break;
160
}
161
case BKLNK_FORUM: {
162
@ <td><a href="%R/info?name=rid:%d(srcid)">forum-%d(srcid)</a>
163
break;
164
}
165
default: {
166
@ <td>unknown(%d(srctype)) - %d(srcid)
167
break;
168
}
169
}
170
@ <td>%h(zMtime)</tr>
171
}
172
@ </tbody>
173
@ </table>
174
db_finalize(&q);
175
style_finish_page();
176
}
177
178
/*
179
** Remove all prior backlinks for the wiki page given. Then
180
** add new backlinks for the latest version of the wiki page.
181
*/
182
void backlink_wiki_refresh(const char *zWikiTitle){
183
int tagid = wiki_tagid(zWikiTitle);
184
int rid;
185
Manifest *pWiki;
186
if( tagid==0 ) return;
187
rid = db_int(0, "SELECT rid FROM tagxref WHERE tagid=%d"
188
" ORDER BY mtime DESC LIMIT 1", tagid);
189
if( rid==0 ) return;
190
pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
191
if( pWiki ){
192
int mimetype = parse_mimetype( pWiki->zMimetype );
193
backlink_extract(pWiki->zWiki, mimetype, tagid, BKLNK_WIKI,
194
pWiki->rDate, 1);
195
manifest_destroy(pWiki);
196
}
197
}
198
199
/*
200
** Structure used to pass down state information through the
201
** markup formatters into the BACKLINK generator.
202
*/
203
#if INTERFACE
204
struct Backlink {
205
int srcid; /* srcid for the source document */
206
int srctype; /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */
207
double mtime; /* mtime field for new BACKLINK table entries */
208
};
209
#endif
210
211
212
/*
213
** zTarget is a hyperlink target in some markup format. If this
214
** target is a self-reference to some other object in the repository,
215
** then create an appropriate backlink.
216
*/
217
void backlink_create(Backlink *p, const char *zTarget, int nTarget){
218
char zLink[HNAME_MAX+4];
219
if( zTarget==0 ) return;
220
if( nTarget<4 ) return;
221
if( nTarget>=10 && strncmp(zTarget,"/info/",6)==0 ){
222
zTarget += 6;
223
nTarget -= 6;
224
}
225
if( nTarget>HNAME_MAX ) return;
226
if( !validate16(zTarget, nTarget) ) return;
227
memcpy(zLink, zTarget, nTarget);
228
zLink[nTarget] = 0;
229
canonical16(zLink, nTarget);
230
db_multi_exec(
231
"REPLACE INTO backlink(target,srctype,srcid,mtime)"
232
"VALUES(%Q,%d,%d,%.17g)", zLink, p->srctype, p->srcid, p->mtime
233
);
234
}
235
236
/*
237
** This routine is called by the markdown formatter for each hyperlink.
238
** If the hyperlink is a backlink, add it to the BACKLINK table.
239
*/
240
static int backlink_md_link(
241
Blob *ob, /* Write output text here (not used in this case) */
242
Blob *target, /* The hyperlink target */
243
Blob *title, /* Hyperlink title */
244
Blob *content, /* Content of the link */
245
void *opaque
246
){
247
Backlink *p = (Backlink*)opaque;
248
char *zTarget = blob_buffer(target);
249
int nTarget = blob_size(target);
250
251
backlink_create(p, zTarget, nTarget);
252
return 1;
253
}
254
255
/* No-op routines for the rendering callbacks that we do not need */
256
static void mkdn_noop_prolog(Blob *b, void *v){ return; }
257
static void (*mkdn_noop_epilog)(Blob*, void*) = mkdn_noop_prolog;
258
static void mkdn_noop_footnotes(Blob *b1, const Blob *b2, void *v){ return; }
259
static void mkdn_noop_blockcode(Blob *b1, Blob *b2, void *v){ return; }
260
static void (*mkdn_noop_blockquote)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
261
static void (*mkdn_noop_blockhtml)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
262
static void mkdn_noop_header(Blob *b1, Blob *b2, int i, void *v){ return; }
263
static void (*mkdn_noop_hrule)(Blob*, void*) = mkdn_noop_prolog;
264
static void (*mkdn_noop_list)(Blob*, Blob*, int, void*) = mkdn_noop_header;
265
static void (*mkdn_noop_listitem)(Blob*, Blob*, int, void*) = mkdn_noop_header;
266
static void (*mkdn_noop_paragraph)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
267
static void mkdn_noop_table(Blob *b1, Blob *b2, Blob *b3, void *v){ return; }
268
static void (*mkdn_noop_table_cell)(Blob*, Blob*, int,
269
void*) = mkdn_noop_header;
270
static void (*mkdn_noop_table_row)(Blob*, Blob*, int,
271
void*) = mkdn_noop_header;
272
static void mkdn_noop_footnoteitm(Blob *b1, const Blob *b2, int i1, int i2,
273
void *v){ return; }
274
static int mkdn_noop_autolink(Blob *b1, Blob *b2, enum mkd_autolink e,
275
void *v){ return 1; }
276
static int mkdn_noop_codespan(Blob *b1, Blob *b2, int i, void *v){ return 1; }
277
static int mkdn_noop_emphasis(Blob *b1, Blob *b2, char c, void *v){ return 1; }
278
static int (*mkdn_noop_dbl_emphas)(Blob*, Blob*, char,
279
void*) = mkdn_noop_emphasis;
280
static int mkdn_noop_image(Blob *b1, Blob *b2, Blob *b3, Blob *b4,
281
void *v){ return 1; }
282
static int mkdn_noop_linebreak(Blob *b1, void *v){ return 1; }
283
static int mkdn_noop_r_html_tag(Blob *b1, Blob *b2, void *v){ return 1; }
284
static int (*mkdn_noop_tri_emphas)(Blob*, Blob*, char,
285
void*) = mkdn_noop_emphasis;
286
static int mkdn_noop_footnoteref(Blob *b1, const Blob *b2, const Blob *b3,
287
int i1, int i2, void *v){ return 1; }
288
static int mkdn_noop_tagref(Blob *b1,Blob *b2, enum mkd_tagspan t,
289
void *p){ return 1; }
290
291
/*
292
** Scan markdown text and add self-hyperlinks to the BACKLINK table.
293
*/
294
void markdown_extract_links(
295
char *zInputText,
296
Backlink *p
297
){
298
struct mkd_renderer html_renderer = {
299
/* prolog */ mkdn_noop_prolog,
300
/* epilog */ mkdn_noop_epilog,
301
/* footnotes */ mkdn_noop_footnotes,
302
303
/* blockcode */ mkdn_noop_blockcode,
304
/* blockquote */ mkdn_noop_blockquote,
305
/* blockhtml */ mkdn_noop_blockhtml,
306
/* header */ mkdn_noop_header,
307
/* hrule */ mkdn_noop_hrule,
308
/* list */ mkdn_noop_list,
309
/* listitem */ mkdn_noop_listitem,
310
/* paragraph */ mkdn_noop_paragraph,
311
/* table */ mkdn_noop_table,
312
/* table_cell */ mkdn_noop_table_cell,
313
/* table_row */ mkdn_noop_table_row,
314
/* footnoteitm*/ mkdn_noop_footnoteitm,
315
316
/* autolink */ mkdn_noop_autolink,
317
/* codespan */ mkdn_noop_codespan,
318
/* dbl_emphas */ mkdn_noop_dbl_emphas,
319
/* emphasis */ mkdn_noop_emphasis,
320
/* image */ mkdn_noop_image,
321
/* linebreak */ mkdn_noop_linebreak,
322
/* link */ backlink_md_link,
323
/* r_html_tag */ mkdn_noop_r_html_tag,
324
/* #tags */ mkdn_noop_tagref,
325
/* tri_emphas */ mkdn_noop_tri_emphas,
326
/* footnoteref*/ mkdn_noop_footnoteref,
327
328
0, /* entity */
329
0, /* normal_text */
330
"*_", /* emphasis characters */
331
0 /* client data */
332
};
333
Blob out, in;
334
html_renderer.opaque = (void*)p;
335
blob_init(&out, 0, 0);
336
blob_init(&in, zInputText, -1);
337
markdown(&out, &in, &html_renderer);
338
blob_reset(&out);
339
blob_reset(&in);
340
}
341
342
/*
343
** Transform mimetype string into an integer code.
344
** NOTE: In the sake of compatibility empty string is parsed as MT_UNKNOWN;
345
** it is yet unclear whether it can safely be changed to MT_NONE.
346
*/
347
int parse_mimetype(const char* zMimetype){
348
if( zMimetype==0 ) return MT_NONE;
349
if( strstr(zMimetype,"wiki")!=0 ) return MT_WIKI;
350
if( strstr(zMimetype,"markdown")!=0 ) return MT_MARKDOWN;
351
return MT_UNKNOWN;
352
}
353
/*
354
** Parse text looking for hyperlinks. Insert references into the
355
** BACKLINK table.
356
*/
357
void backlink_extract(
358
char *zSrc, /* Input text from which links are extracted */
359
int mimetype, /* Mimetype of input. MT_NONE works as MT_WIKI */
360
int srcid, /* srcid for the source document */
361
int srctype, /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */
362
double mtime, /* mtime field for new BACKLINK table entries */
363
int replaceFlag /* True to overwrite prior BACKLINK entries */
364
){
365
Backlink bklnk;
366
if( replaceFlag ){
367
db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid=%d",
368
srctype, srcid);
369
}
370
bklnk.srcid = srcid;
371
assert( ValidBklnk(srctype) );
372
assert( ValidMTC(mimetype) );
373
bklnk.srctype = srctype;
374
bklnk.mtime = mtime;
375
if( mimetype==MT_NONE || mimetype==MT_WIKI ){
376
wiki_extract_links(zSrc, &bklnk, srctype==BKLNK_COMMENT ? WIKI_INLINE : 0);
377
}else if( mimetype==MT_MARKDOWN ){
378
markdown_extract_links(zSrc, &bklnk);
379
}
380
}
381
382
/*
383
** COMMAND: test-backlinks
384
**
385
** Usage: %fossil test-backlinks SRCTYPE SRCID ?OPTIONS? INPUT-FILE
386
**
387
** Read the content of INPUT-FILE and pass it into the backlink_extract()
388
** routine. But instead of adding backlinks to the backlink table,
389
** just print them on stdout. SRCID and SRCTYPE are integers.
390
**
391
** Options:
392
** --mtime DATETIME Use an alternative date/time. Defaults to the
393
** current date/time.
394
** --mimetype TYPE Use an alternative mimetype
395
*/
396
void test_backlinks_cmd(void){
397
const char *zMTime = find_option("mtime",0,1);
398
const char *zMimetype = find_option("mimetype",0,1);
399
const int mimetype = parse_mimetype(zMimetype);
400
Blob in;
401
int srcid;
402
int srctype;
403
double mtime;
404
405
verify_all_options();
406
if( g.argc!=5 ){
407
usage("SRCTYPE SRCID INPUTFILE");
408
}
409
srctype = atoi(g.argv[2]);
410
if( srctype<0 || srctype>2 ){
411
fossil_fatal("SRCTYPE should be an integer 0, 1, or 2");
412
}
413
srcid = atoi(g.argv[3]);
414
blob_read_from_file(&in, g.argv[4], ExtFILE);
415
sqlite3_open(":memory:",&g.db);
416
if( zMTime==0 ) zMTime = "now";
417
mtime = db_double(1721059.5,"SELECT julianday(%Q)",zMTime);
418
g.fSqlPrint = 1;
419
sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0);
420
db_multi_exec(
421
"CREATE TEMP TABLE backlink(target,srctype,srcid,mtime);\n"
422
"CREATE TRIGGER backlink_insert BEFORE INSERT ON backlink BEGIN\n"
423
" SELECT print("
424
" 'target='||quote(new.target)||"
425
" ' srctype='||quote(new.srctype)||"
426
" ' srcid='||quote(new.srcid)||"
427
" ' mtime='||datetime(new.mtime));\n"
428
" SELECT raise(ignore);\n"
429
"END;"
430
);
431
backlink_extract(blob_str(&in),mimetype,srcid,srctype,mtime,0);
432
blob_reset(&in);
433
}
434
435
436
/*
437
** COMMAND: test-relink-wiki
438
**
439
** Usage: %fossil test-relink-wiki WIKI-PAGE-NAME
440
**
441
** Run the backlink_wiki_refresh() procedure on the wiki page
442
** named. WIKI-PAGE-NAME can be a glob pattern or a prefix
443
** of the wiki page.
444
*/
445
void test_wiki_relink_cmd(void){
446
Stmt q;
447
db_find_and_open_repository(0, 0);
448
if( g.argc!=3 ) usage("WIKI-PAGE-NAME");
449
db_prepare(&q,
450
"SELECT substr(tagname,6) FROM tag WHERE tagname GLOB 'wiki-%q*'",
451
g.argv[2]
452
);
453
while( db_step(&q)==SQLITE_ROW ){
454
const char *zPage = db_column_text(&q,0);
455
fossil_print("Relinking page: %s\n", zPage);
456
backlink_wiki_refresh(zPage);
457
}
458
db_finalize(&q);
459
}
460

Keyboard Shortcuts

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