/* ** 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 /* ** 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("
\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, 0, 0, 0, 0, 0); db_finalize(&q); if( needEndPanel ){ cgi_printf("
\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, 0, 0, 0, 0, 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"); @

%d(n) backlink table entries:

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(); @ @ @ 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); @ } @ @
Target Source mtime
%h(zTarget) switch( srctype ){ case BKLNK_COMMENT: { @ checkin-%d(srcid) break; } case BKLNK_TICKET: { @ ticket-%d(srcid) break; } case BKLNK_WIKI: { const char *zName = db_column_text(&q, 4); @ wiki-%d(srcid) break; } case BKLNK_EVENT: { @ tecknote-%d(srcid) break; } case BKLNK_FORUM: { @ forum-%d(srcid) break; } default: { @ unknown(%d(srctype)) - %d(srcid) break; } } @ %h(zMtime)
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 compatability 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-wiki-relink ** ** Usage: %fossil test-wiki-relink 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); }