Fossil SCM

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

Keyboard Shortcuts

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