/* ** 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@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains subroutines used for recognizing, configuring, and ** handling interwiki hyperlinks. */ #include "config.h" #include "interwiki.h" /* ** If zTarget is an interwiki link, return a pointer to a URL for that ** link target in memory obtained from fossil_malloc(). If zTarget is ** not a valid interwiki link, return NULL. ** ** An interwiki link target is of the form: ** ** Code:PageName ** ** "Code" is a brief code that describes the intended target wiki. ** The code must be ASCII alpha-numeric. No symbols or non-ascii ** characters are allows. Case is ignored for the code. ** Codes are assigned by "intermap:*" entries in the CONFIG table. ** The link is only valid if there exists an entry in the CONFIG table ** that matches "intermap:Code". ** ** Each value of each intermap:Code entry in the CONFIG table is a JSON ** object with the following fields: ** ** { ** "base": Base URL for the remote site. ** "hash": Append this to "base" for Hash targets. ** "wiki": Append this to "base" for Wiki targets. ** } ** ** If the remote wiki is Fossil, then the correct value for "hash" ** is "/info/" and the correct value for "wiki" is "/wiki?name=". ** If (for example) Wikipedia is the remote, then "hash" should be ** omitted and the correct value for "wiki" is "/wiki/". ** ** PageName is link name of the target wiki. Several different forms ** of PageName are recognized. ** ** Path If PageName is empty or begins with a "/" character, then ** it is a pathname that is appended to "base". ** ** Hash If PageName is a hexadecimal string of 4 or more ** characters, then PageName is appended to "hash" which ** is then appended to "base". ** ** Wiki If PageName does not start with "/" and it is ** not a hexadecimal string of 4 or more characters, then ** PageName is appended to "wiki" and that combination is ** appended to "base". ** ** See https://en.wikipedia.org/wiki/Interwiki_links for further information ** on interwiki links. */ char *interwiki_url(const char *zTarget){ int nCode; int i; const char *zPage; int nPage; char *zUrl = 0; char *zName; static Stmt q; for(i=0; fossil_isalnum(zTarget[i]); i++){} if( zTarget[i]!=':' ) return 0; nCode = i; if( nCode==4 && strncmp(zTarget,"wiki",4)==0 ) return 0; zPage = zTarget + nCode + 1; nPage = (int)strlen(zPage); db_static_prepare(&q, "SELECT json_extract(value,'$.base')," " json_extract(value,'$.hash')," " json_extract(value,'$.wiki')" " FROM config WHERE name=lower($name)" ); zName = mprintf("interwiki:%.*s", nCode, zTarget); db_bind_text(&q, "$name", zName); while( db_step(&q)==SQLITE_ROW ){ const char *zBase = db_column_text(&q,0); if( zBase==0 || zBase[0]==0 ) break; if( nPage==0 || zPage[0]=='/' ){ /* Path */ zUrl = mprintf("%s%s", zBase, zPage); }else if( nPage>=4 && validate16(zPage,nPage) ){ /* Hash */ const char *zHash = db_column_text(&q,1); if( zHash && zHash[0] ){ zUrl = mprintf("%s%s%s", zBase, zHash, zPage); } }else{ /* Wiki */ const char *zWiki = db_column_text(&q,2); if( zWiki && zWiki[0] ){ zUrl = mprintf("%s%s%s", zBase, zWiki, zPage); } } break; } db_reset(&q); free(zName); return zUrl; } /* ** If hyperlink target zTarget begins with an interwiki tag that ought ** to be excluded from display, then return the number of characters in ** that tag. ** ** Path interwiki targets always return zero. In other words, links ** of the form: ** ** remote:/path/to/file.txt ** ** Do not have the interwiki tag removed. But Hash and Wiki links are ** transformed: ** ** src:39cb0a323f2f3fb6 -> 39cb0a323f2f3fb6 ** fossil:To Do List -> To Do List */ int interwiki_removable_prefix(const char *zTarget){ int i; for(i=0; fossil_isalnum(zTarget[i]); i++){} if( zTarget[i]!=':' ) return 0; i++; if( zTarget[i]==0 || zTarget[i]=='/' ) return 0; return i; } /* ** Verify that a name is a valid interwiki "Code". Rules: ** ** * ascii ** * alphanumeric */ static int interwiki_valid_name(const char *zName){ int i; for(i=0; zName[i]; i++){ if( !fossil_isalnum(zName[i]) ) return 0; } return 1; } /* ** COMMAND: interwiki* ** ** Usage: %fossil interwiki COMMAND ... ** ** Manage the "intermap" that defines the mapping from interwiki tags ** to complete URLs for interwiki links. ** ** > fossil interwiki delete TAG ... ** ** Delete one or more interwiki maps. ** ** > fossil interwiki edit TAG --base URL --hash PATH --wiki PATH ** ** Create a interwiki referenced call TAG. The base URL is ** the --base option, which is required. The --hash and --wiki ** paths are optional. The TAG must be lower-case alphanumeric ** and must be unique. A new entry is created if it does not ** already exit. ** ** > fossil interwiki list ** ** Show all interwiki mappings. */ void interwiki_cmd(void){ const char *zCmd; int nCmd; db_find_and_open_repository(0, 0); if( g.argc<3 ){ usage("SUBCOMMAND ..."); } zCmd = g.argv[2]; nCmd = (int)strlen(zCmd); if( strncmp(zCmd,"edit",nCmd)==0 ){ const char *zName; const char *zBase = find_option("base",0,1); const char *zHash = find_option("hash",0,1); const char *zWiki = find_option("wiki",0,1); verify_all_options(); if( g.argc!=4 ) usage("add TAG ?OPTIONS?"); zName = g.argv[3]; if( zBase==0 ){ fossil_fatal("the --base option is required"); } if( !interwiki_valid_name(zName) ){ fossil_fatal("not a valid interwiki tag: \"%s\"", zName); } db_begin_write(); db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value,mtime)" " VALUES('interwiki:'||lower(%Q)," " json_object('base',%Q,'hash',%Q,'wiki',%Q)," " now());", zName, zBase, zHash, zWiki ); setup_incr_cfgcnt(); db_protect_pop(); db_commit_transaction(); }else if( strncmp(zCmd, "delete", nCmd)==0 ){ int i; verify_all_options(); if( g.argc<4 ) usage("delete ID ..."); db_begin_write(); db_unprotect(PROTECT_CONFIG); for(i=3; i\n"); } blob_appendf(out,"", db_column_text(&q,0)); blob_appendf(out,"\n", db_column_text(&q,1)); n++; } db_finalize(&q); if( n>0 ){ blob_appendf(out,"
%h → %h
\n"); }else{ blob_appendf(out,"None\n"); } } /* ** WEBPAGE: /intermap ** ** View and modify the interwiki tag map or "intermap". ** This page is visible to administrators only. */ void interwiki_page(void){ Stmt q; int n = 0; const char *z; const char *zTag = ""; const char *zBase = ""; const char *zHash = ""; const char *zWiki = ""; char *zErr = 0; login_check_credentials(); if( !g.perm.Read && !g.perm.RdWiki && ~g.perm.RdTkt ){ login_needed(0); return; } if( g.perm.Setup && P("submit")!=0 && cgi_csrf_safe(1) ){ zTag = PT("tag"); zBase = PT("base"); zHash = PT("hash"); zWiki = PT("wiki"); if( zTag==0 || zTag[0]==0 || !interwiki_valid_name(zTag) ){ zErr = mprintf("Not a valid interwiki tag name: \"%s\"", zTag?zTag : ""); }else if( zBase==0 || zBase[0]==0 ){ db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name='interwiki:%q';", zTag); db_protect_pop(); }else{ if( zHash && zHash[0]==0 ) zHash = 0; if( zWiki && zWiki[0]==0 ) zWiki = 0; db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value,mtime)" "VALUES('interwiki:'||lower(%Q)," " json_object('base',%Q,'hash',%Q,'wiki',%Q)," " now());", zTag, zBase, zHash, zWiki); db_protect_pop(); } } style_set_current_feature("interwiki"); style_header("Interwiki Map Configuration"); @

Interwiki links are hyperlink targets of the form @

Tag:PageName
@

Such links resolve to links to PageName on a separate server @ identified by Tag. The Interwiki Map or "intermap" is a mapping @ from Tags to complete Server URLs. db_prepare(&q, "SELECT substr(name,11)," " json_extract(value,'$.base')," " json_extract(value,'$.hash')," " json_extract(value,'$.wiki')" " FROM config WHERE name glob 'interwiki:*'" ); while( db_step(&q)==SQLITE_ROW ){ if( n==0 ){ @ The current mapping is as follows: @

    } @
  1. %h(db_column_text(&q,0)) @

      @
    • Base-URL: %h(db_column_text(&q,1)) z = db_column_text(&q,2); if( z==0 ){ @
    • Hash-path: NULL }else{ @
    • Hash-path: %h(z) } z = db_column_text(&q,3); if( z==0 ){ @
    • Wiki-path: NULL }else{ @
    • Wiki-path: %h(z) } @
    n++; } db_finalize(&q); if( n ){ @
}else{ @ No mappings are currently defined. } if( !g.perm.Setup ){ /* Do not show intermap editing fields to non-setup users */ style_finish_page(); return; } @

To add a new mapping, fill out the form below providing a unique name @ for the tag. To edit an exist mapping, fill out the form and use the @ existing name as the tag. To delete an existing mapping, fill in the @ tag field but leave the "Base URL" field blank.

if( zErr ){ @

%h(zErr)

} @
login_insert_csrf_secret(); @ @ @ @ @ @ @ @ @ @ @ @
Tag:
Base URL:
Hash-path: @ (use "/info/" when the target is Fossil)
Wiki-path: @ (use "/wiki?name=" when the target is Fossil)
@
style_finish_page(); }