/*
** 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<g.argc; i++){
const char *zName = g.argv[i];
db_multi_exec(
"DELETE FROM config WHERE name='interwiki:%q'",
zName
);
}
setup_incr_cfgcnt();
db_protect_pop();
db_commit_transaction();
}else
if( strncmp(zCmd, "list", nCmd)==0 ){
Stmt q;
int n = 0;
verify_all_options();
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 ){
const char *zBase, *z, *zName;
if( n++ ) fossil_print("\n");
zName = db_column_text(&q,0);
zBase = db_column_text(&q,1);
fossil_print("%-15s %s\n", zName, zBase);
z = db_column_text(&q,2);
if( z ){
fossil_print("%15s %s%s\n", "", zBase, z);
}
z = db_column_text(&q,3);
if( z ){
fossil_print("%15s %s%s\n", "", zBase, z);
}
}
db_finalize(&q);
}else
{
fossil_fatal("unknown command \"%s\" - should be one of: "
"delete edit list", zCmd);
}
}
/*
** Append text to the "Markdown" or "Wiki" rules pages that shows
** a table of all interwiki tags available on this system.
*/
void interwiki_append_map_table(Blob *out){
int n = 0;
Stmt q;
db_prepare(&q,
"SELECT substr(name,11), json_extract(value,'$.base')"
" FROM config WHERE name glob 'interwiki:*'"
" ORDER BY name;"
);
while( db_step(&q)==SQLITE_ROW ){
if( n==0 ){
blob_appendf(out, "<blockquote><table>\n");
}
blob_appendf(out,"<tr><td>%h</td><td> → </td>",
db_column_text(&q,0));
blob_appendf(out,"<td>%h</td></tr>\n",
db_column_text(&q,1));
n++;
}
db_finalize(&q);
if( n>0 ){
blob_appendf(out,"</table></blockquote>\n");
}else{
blob_appendf(out,"<i>None</i></blockquote>\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_header("Interwiki Map Configuration");
@ <p>Interwiki links are hyperlink targets of the form
@ <blockquote><i>Tag</i><b>:</b><i>PageName</i></blockquote>
@ <p>Such links resolve to links to <i>PageName</i> on a separate server
@ identified by <i>Tag</i>. The Interwiki Map or "intermap" is a mapping
@ from <i>Tags</i> 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:
@ <ol>
}
@ <li><p> %h(db_column_text(&q,0))
@ <ul>
@ <li> Base-URL: <tt>%h(db_column_text(&q,1))</tt>
z = db_column_text(&q,2);
if( z==0 ){
@ <li> Hash-path: <i>NULL</i>
}else{
@ <li> Hash-path: <tt>%h(z)</tt>
}
z = db_column_text(&q,3);
if( z==0 ){
@ <li> Wiki-path: <i>NULL</i>
}else{
@ <li> Wiki-path: <tt>%h(z)</tt>
}
@ </ul>
n++;
}
db_finalize(&q);
if( n ){
@ </ol>
}else{
@ No mappings are currently defined.
}
if( !g.perm.Setup ){
/* Do not show intermap editing fields to non-setup users */
style_finish_page("interwiki");
return;
}
@ <p>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.</p>
if( zErr ){
@ <p class="error">%h(zErr)</p>
}
@ <form method="POST" action="%R/intermap">
login_insert_csrf_secret();
@ <table border="0">
@ <tr><td class="form_label" id="imtag">Tag:</td>
@ <td><input type="text" id="tag" aria-labeledby="imtag" name="tag" \
@ size="15" value="%h(zTag)"></td></tr>
@ <tr><td class="form_label" id="imbase">Base URL:</td>
@ <td><input type="text" id="base" aria-labeledby="imbase" name="base" \
@ size="70" value="%h(zBase)"></td></tr>
@ <tr><td class="form_label" id="imhash">Hash-path:</td>
@ <td><input type="text" id="hash" aria-labeledby="imhash" name="hash" \
@ size="20" value="%h(zHash)">
@ (use "<tt>/info/</tt>" when the target is Fossil)</td></tr>
@ <tr><td class="form_label" id="imwiki">Wiki-path:</td>
@ <td><input type="text" id="wiki" aria-labeledby="imwiki" name="wiki" \
@ size="20" value="%h(zWiki)">
@ (use "<tt>/wiki?name=</tt>" when the target is Fossil)</td></tr>
@ <tr><td></td>
@ <td><input type="submit" name="submit" value="Apply Changes"></td></tr>
@ </table>
@ </form>
style_finish_page("interwiki");
}