summaryrefslogtreecommitdiff
path: root/src/backlink.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backlink.c')
-rw-r--r--src/backlink.c456
1 files changed, 456 insertions, 0 deletions
diff --git a/src/backlink.c b/src/backlink.c
new file mode 100644
index 0000000..82ad714
--- /dev/null
+++ b/src/backlink.c
@@ -0,0 +1,456 @@
+/*
+** Copyright (c) 2020 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+
+** This program is distributed in the hope that it will be useful,
+** but without any warranty; without even the implied warranty of
+** merchantability or fitness for a particular purpose.
+**
+** Author contact information:
+** drh@sqlite.org
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** This file contains code to implement for managing backlinks and
+** the "backlink" table of the repository database.
+**
+** A backlink is a reference in Fossil-Wiki or Markdown to some other
+** object in the repository.
+*/
+#include "config.h"
+#include "backlink.h"
+#include <assert.h>
+
+
+/*
+** Show a graph of all wiki, tickets, and check-ins that refer to object zUuid.
+**
+** If zLabel is not NULL and the graph is not empty, then output zLabel as
+** a prefix to the graph.
+*/
+void render_backlink_graph(const char *zUuid, const char *zLabel){
+ Blob sql;
+ Stmt q;
+ char *zGlob;
+ int needEndPanel = 0;
+ zGlob = mprintf("%.5s*", zUuid);
+ db_multi_exec(
+ "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);\n"
+ "DELETE FROM ok;\n"
+ "INSERT OR IGNORE INTO ok(rid)\n"
+ " SELECT CASE srctype\n"
+ " WHEN 2 THEN (SELECT rid FROM tagxref WHERE tagid=backlink.srcid\n"
+ " ORDER BY mtime DESC LIMIT 1)\n"
+ " ELSE srcid END\n"
+ " FROM backlink\n"
+ " WHERE target GLOB %Q"
+ " AND %Q GLOB (target || '*');",
+ zGlob, zUuid
+ );
+ if( !db_exists("SELECT 1 FROM ok") ) return;
+ if( zLabel ){
+ cgi_printf("%s", zLabel);
+ if( strstr(zLabel, "accordion")!=0 ){
+ cgi_printf("<div class=\"accordion_panel\">\n");
+ needEndPanel = 1;
+ }
+ }
+ blob_zero(&sql);
+ blob_append(&sql, timeline_query_for_www(), -1);
+ blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
+ db_prepare(&q, "%s", blob_sql_text(&sql));
+ www_print_timeline(&q,
+ TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL|TIMELINE_REFS, 0);
+ db_finalize(&q);
+ if( needEndPanel ){
+ cgi_printf("</div>\n");
+ }
+}
+
+/*
+** WEBPAGE: test-backlink-timeline
+**
+** Show a timeline of all check-ins and other events that have entries
+** in the backlink table. This is used for testing the rendering
+** of the "References" section of the /info page.
+*/
+void backlink_timeline_page(void){
+ Blob sql;
+ Stmt q;
+
+ login_check_credentials();
+ if( !g.perm.Read || !g.perm.RdTkt || !g.perm.RdWiki ){
+ login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
+ return;
+ }
+ style_set_current_feature("test");
+ style_header("Backlink Timeline (Internal Testing Use)");
+ db_multi_exec(
+ "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);"
+ "DELETE FROM ok;"
+ "INSERT OR IGNORE INTO ok"
+ " SELECT blob.rid FROM backlink, blob"
+ " WHERE blob.uuid BETWEEN backlink.target AND (backlink.target||'x')"
+ );
+ blob_zero(&sql);
+ blob_append(&sql, timeline_query_for_www(), -1);
+ blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
+ db_prepare(&q, "%s", blob_sql_text(&sql));
+ www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL,0);
+ db_finalize(&q);
+ style_finish_page();
+}
+
+/*
+** WEBPAGE: test-backlinks
+**
+** Show a table of all backlinks. Admin access only.
+*/
+void backlink_table_page(void){
+ Stmt q;
+ int n;
+ login_check_credentials();
+ if( !g.perm.Admin ){
+ login_needed(g.anon.Admin);
+ return;
+ }
+ style_set_current_feature("test");
+ style_header("Backlink Table (Internal Testing Use)");
+ n = db_int(0, "SELECT count(*) FROM backlink");
+ @ <p>%d(n) backlink table entries:</p>
+ db_prepare(&q,
+ "SELECT target, srctype, srcid, datetime(mtime),"
+ " CASE srctype"
+ " WHEN 2 THEN (SELECT substr(tagname,6) FROM tag"
+ " WHERE tagid=srcid AND tagname GLOB 'wiki-*')"
+ " ELSE null END FROM backlink"
+ );
+ style_table_sorter();
+ @ <table border="1" cellpadding="2" cellspacing="0" \
+ @ class='sortable' data-column-types='ttt' data-init-sort='0'>
+ @ <thead><tr><th> Target <th> Source <th> mtime </tr></thead>
+ @ <tbody>
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zTarget = db_column_text(&q, 0);
+ int srctype = db_column_int(&q, 1);
+ int srcid = db_column_int(&q, 2);
+ const char *zMtime = db_column_text(&q, 3);
+ @ <tr><td><a href="%R/info/%h(zTarget)">%h(zTarget)</a>
+ switch( srctype ){
+ case BKLNK_COMMENT: {
+ @ <td><a href="%R/info?name=rid:%d(srcid)">checkin-%d(srcid)</a>
+ break;
+ }
+ case BKLNK_TICKET: {
+ @ <td><a href="%R/info?name=rid:%d(srcid)">ticket-%d(srcid)</a>
+ break;
+ }
+ case BKLNK_WIKI: {
+ const char *zName = db_column_text(&q, 4);
+ @ <td><a href="%R/wiki?name=%h(zName)&p">wiki-%d(srcid)</a>
+ break;
+ }
+ case BKLNK_EVENT: {
+ @ <td><a href="%R/info?name=rid:%d(srcid)">tecknote-%d(srcid)</a>
+ break;
+ }
+ case BKLNK_FORUM: {
+ @ <td><a href="%R/info?name=rid:%d(srcid)">forum-%d(srcid)</a>
+ break;
+ }
+ default: {
+ @ <td>unknown(%d(srctype)) - %d(srcid)
+ break;
+ }
+ }
+ @ <td>%h(zMtime)</tr>
+ }
+ @ </tbody>
+ @ </table>
+ db_finalize(&q);
+ style_finish_page();
+}
+
+/*
+** Remove all prior backlinks for the wiki page given. Then
+** add new backlinks for the latest version of the wiki page.
+*/
+void backlink_wiki_refresh(const char *zWikiTitle){
+ int tagid = wiki_tagid(zWikiTitle);
+ int rid;
+ Manifest *pWiki;
+ if( tagid==0 ) return;
+ rid = db_int(0, "SELECT rid FROM tagxref WHERE tagid=%d"
+ " ORDER BY mtime DESC LIMIT 1", tagid);
+ if( rid==0 ) return;
+ pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
+ if( pWiki ){
+ int mimetype = parse_mimetype( pWiki->zMimetype );
+ backlink_extract(pWiki->zWiki, mimetype, tagid, BKLNK_WIKI,
+ pWiki->rDate, 1);
+ manifest_destroy(pWiki);
+ }
+}
+
+/*
+** Structure used to pass down state information through the
+** markup formatters into the BACKLINK generator.
+*/
+#if INTERFACE
+struct Backlink {
+ int srcid; /* srcid for the source document */
+ int srctype; /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */
+ double mtime; /* mtime field for new BACKLINK table entries */
+};
+#endif
+
+
+/*
+** zTarget is a hyperlink target in some markup format. If this
+** target is a self-reference to some other object in the repository,
+** then create an appropriate backlink.
+*/
+void backlink_create(Backlink *p, const char *zTarget, int nTarget){
+ char zLink[HNAME_MAX+4];
+ if( zTarget==0 ) return;
+ if( nTarget<4 ) return;
+ if( nTarget>=10 && strncmp(zTarget,"/info/",6)==0 ){
+ zTarget += 6;
+ nTarget -= 6;
+ }
+ if( nTarget>HNAME_MAX ) return;
+ if( !validate16(zTarget, nTarget) ) return;
+ memcpy(zLink, zTarget, nTarget);
+ zLink[nTarget] = 0;
+ canonical16(zLink, nTarget);
+ db_multi_exec(
+ "REPLACE INTO backlink(target,srctype,srcid,mtime)"
+ "VALUES(%Q,%d,%d,%.17g)", zLink, p->srctype, p->srcid, p->mtime
+ );
+}
+
+/*
+** This routine is called by the markdown formatter for each hyperlink.
+** If the hyperlink is a backlink, add it to the BACKLINK table.
+*/
+static int backlink_md_link(
+ Blob *ob, /* Write output text here (not used in this case) */
+ Blob *target, /* The hyperlink target */
+ Blob *title, /* Hyperlink title */
+ Blob *content, /* Content of the link */
+ void *opaque
+){
+ Backlink *p = (Backlink*)opaque;
+ char *zTarget = blob_buffer(target);
+ int nTarget = blob_size(target);
+
+ backlink_create(p, zTarget, nTarget);
+ return 1;
+}
+
+/* No-op routines for the rendering callbacks that we do not need */
+static void mkdn_noop_prolog(Blob *b, void *v){ return; }
+static void (*mkdn_noop_epilog)(Blob*, void*) = mkdn_noop_prolog;
+static void mkdn_noop_footnotes(Blob *b1, const Blob *b2, void *v){ return; }
+static void mkdn_noop_blockcode(Blob *b1, Blob *b2, void *v){ return; }
+static void (*mkdn_noop_blockquote)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
+static void (*mkdn_noop_blockhtml)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
+static void mkdn_noop_header(Blob *b1, Blob *b2, int i, void *v){ return; }
+static void (*mkdn_noop_hrule)(Blob*, void*) = mkdn_noop_prolog;
+static void (*mkdn_noop_list)(Blob*, Blob*, int, void*) = mkdn_noop_header;
+static void (*mkdn_noop_listitem)(Blob*, Blob*, int, void*) = mkdn_noop_header;
+static void (*mkdn_noop_paragraph)(Blob*, Blob*, void*) = mkdn_noop_blockcode;
+static void mkdn_noop_table(Blob *b1, Blob *b2, Blob *b3, void *v){ return; }
+static void (*mkdn_noop_table_cell)(Blob*, Blob*, int,
+ void*) = mkdn_noop_header;
+static void (*mkdn_noop_table_row)(Blob*, Blob*, int,
+ void*) = mkdn_noop_header;
+static void mkdn_noop_footnoteitm(Blob *b1, const Blob *b2, int i1, int i2,
+ void *v){ return; }
+static int mkdn_noop_autolink(Blob *b1, Blob *b2, enum mkd_autolink e,
+ void *v){ return 1; }
+static int mkdn_noop_codespan(Blob *b1, Blob *b2, int i, void *v){ return 1; }
+static int mkdn_noop_emphasis(Blob *b1, Blob *b2, char c, void *v){ return 1; }
+static int (*mkdn_noop_dbl_emphas)(Blob*, Blob*, char,
+ void*) = mkdn_noop_emphasis;
+static int mkdn_noop_image(Blob *b1, Blob *b2, Blob *b3, Blob *b4,
+ void *v){ return 1; }
+static int mkdn_noop_linebreak(Blob *b1, void *v){ return 1; }
+static int mkdn_noop_r_html_tag(Blob *b1, Blob *b2, void *v){ return 1; }
+static int (*mkdn_noop_tri_emphas)(Blob*, Blob*, char,
+ void*) = mkdn_noop_emphasis;
+static int mkdn_noop_footnoteref(Blob *b1, const Blob *b2, const Blob *b3,
+ int i1, int i2, void *v){ return 1; }
+
+/*
+** Scan markdown text and add self-hyperlinks to the BACKLINK table.
+*/
+void markdown_extract_links(
+ char *zInputText,
+ Backlink *p
+){
+ struct mkd_renderer html_renderer = {
+ /* prolog */ mkdn_noop_prolog,
+ /* epilog */ mkdn_noop_epilog,
+ /* footnotes */ mkdn_noop_footnotes,
+
+ /* blockcode */ mkdn_noop_blockcode,
+ /* blockquote */ mkdn_noop_blockquote,
+ /* blockhtml */ mkdn_noop_blockhtml,
+ /* header */ mkdn_noop_header,
+ /* hrule */ mkdn_noop_hrule,
+ /* list */ mkdn_noop_list,
+ /* listitem */ mkdn_noop_listitem,
+ /* paragraph */ mkdn_noop_paragraph,
+ /* table */ mkdn_noop_table,
+ /* table_cell */ mkdn_noop_table_cell,
+ /* table_row */ mkdn_noop_table_row,
+ /* footnoteitm*/ mkdn_noop_footnoteitm,
+
+ /* autolink */ mkdn_noop_autolink,
+ /* codespan */ mkdn_noop_codespan,
+ /* dbl_emphas */ mkdn_noop_dbl_emphas,
+ /* emphasis */ mkdn_noop_emphasis,
+ /* image */ mkdn_noop_image,
+ /* linebreak */ mkdn_noop_linebreak,
+ /* link */ backlink_md_link,
+ /* r_html_tag */ mkdn_noop_r_html_tag,
+ /* tri_emphas */ mkdn_noop_tri_emphas,
+ /* footnoteref*/ mkdn_noop_footnoteref,
+
+ 0, /* entity */
+ 0, /* normal_text */
+ "*_", /* emphasis characters */
+ 0 /* client data */
+ };
+ Blob out, in;
+ html_renderer.opaque = (void*)p;
+ blob_init(&out, 0, 0);
+ blob_init(&in, zInputText, -1);
+ markdown(&out, &in, &html_renderer);
+ blob_reset(&out);
+ blob_reset(&in);
+}
+
+/*
+** Transform mimetype string into an integer code.
+** NOTE: In the sake of compatibility empty string is parsed as MT_UNKNOWN;
+** it is yet unclear whether it can safely be changed to MT_NONE.
+*/
+int parse_mimetype(const char* zMimetype){
+ if( zMimetype==0 ) return MT_NONE;
+ if( strstr(zMimetype,"wiki")!=0 ) return MT_WIKI;
+ if( strstr(zMimetype,"markdown")!=0 ) return MT_MARKDOWN;
+ return MT_UNKNOWN;
+}
+/*
+** Parse text looking for hyperlinks. Insert references into the
+** BACKLINK table.
+*/
+void backlink_extract(
+ char *zSrc, /* Input text from which links are extracted */
+ int mimetype, /* Mimetype of input. MT_NONE works as MT_WIKI */
+ int srcid, /* srcid for the source document */
+ int srctype, /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */
+ double mtime, /* mtime field for new BACKLINK table entries */
+ int replaceFlag /* True to overwrite prior BACKLINK entries */
+){
+ Backlink bklnk;
+ if( replaceFlag ){
+ db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid=%d",
+ srctype, srcid);
+ }
+ bklnk.srcid = srcid;
+ assert( ValidBklnk(srctype) );
+ assert( ValidMTC(mimetype) );
+ bklnk.srctype = srctype;
+ bklnk.mtime = mtime;
+ if( mimetype==MT_NONE || mimetype==MT_WIKI ){
+ wiki_extract_links(zSrc, &bklnk, srctype==BKLNK_COMMENT ? WIKI_INLINE : 0);
+ }else if( mimetype==MT_MARKDOWN ){
+ markdown_extract_links(zSrc, &bklnk);
+ }
+}
+
+/*
+** COMMAND: test-backlinks
+**
+** Usage: %fossil test-backlinks SRCTYPE SRCID ?OPTIONS? INPUT-FILE
+**
+** Read the content of INPUT-FILE and pass it into the backlink_extract()
+** routine. But instead of adding backlinks to the backlink table,
+** just print them on stdout. SRCID and SRCTYPE are integers.
+**
+** Options:
+** --mtime DATETIME Use an alternative date/time. Defaults to the
+** current date/time.
+** --mimetype TYPE Use an alternative mimetype
+*/
+void test_backlinks_cmd(void){
+ const char *zMTime = find_option("mtime",0,1);
+ const char *zMimetype = find_option("mimetype",0,1);
+ const int mimetype = parse_mimetype(zMimetype);
+ Blob in;
+ int srcid;
+ int srctype;
+ double mtime;
+
+ verify_all_options();
+ if( g.argc!=5 ){
+ usage("SRCTYPE SRCID INPUTFILE");
+ }
+ srctype = atoi(g.argv[2]);
+ if( srctype<0 || srctype>2 ){
+ fossil_fatal("SRCTYPE should be an integer 0, 1, or 2");
+ }
+ srcid = atoi(g.argv[3]);
+ blob_read_from_file(&in, g.argv[4], ExtFILE);
+ sqlite3_open(":memory:",&g.db);
+ if( zMTime==0 ) zMTime = "now";
+ mtime = db_double(1721059.5,"SELECT julianday(%Q)",zMTime);
+ g.fSqlPrint = 1;
+ sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0);
+ db_multi_exec(
+ "CREATE TEMP TABLE backlink(target,srctype,srcid,mtime);\n"
+ "CREATE TRIGGER backlink_insert BEFORE INSERT ON backlink BEGIN\n"
+ " SELECT print("
+ " 'target='||quote(new.target)||"
+ " ' srctype='||quote(new.srctype)||"
+ " ' srcid='||quote(new.srcid)||"
+ " ' mtime='||datetime(new.mtime));\n"
+ " SELECT raise(ignore);\n"
+ "END;"
+ );
+ backlink_extract(blob_str(&in),mimetype,srcid,srctype,mtime,0);
+ blob_reset(&in);
+}
+
+
+/*
+** COMMAND: test-relink-wiki
+**
+** Usage: %fossil test-relink-wiki WIKI-PAGE-NAME
+**
+** Run the backlink_wiki_refresh() procedure on the wiki page
+** named. WIKI-PAGE-NAME can be a glob pattern or a prefix
+** of the wiki page.
+*/
+void test_wiki_relink_cmd(void){
+ Stmt q;
+ db_find_and_open_repository(0, 0);
+ if( g.argc!=3 ) usage("WIKI-PAGE-NAME");
+ db_prepare(&q,
+ "SELECT substr(tagname,6) FROM tag WHERE tagname GLOB 'wiki-%q*'",
+ g.argv[2]
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zPage = db_column_text(&q,0);
+ fossil_print("Relinking page: %s\n", zPage);
+ backlink_wiki_refresh(zPage);
+ }
+ db_finalize(&q);
+}