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 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains subroutines used for recognizing, configuring, and
19 ** handling interwiki hyperlinks.
20 */
21 #include "config.h"
22 #include "interwiki.h"
23 
24 
25 /*
26 ** If zTarget is an interwiki link, return a pointer to a URL for that
27 ** link target in memory obtained from fossil_malloc().  If zTarget is
28 ** not a valid interwiki link, return NULL.
29 **
30 ** An interwiki link target is of the form:
31 **
32 **       Code:PageName
33 **
34 ** "Code" is a brief code that describes the intended target wiki.
35 ** The code must be ASCII alpha-numeric.  No symbols or non-ascii
36 ** characters are allows.  Case is ignored for the code.
37 ** Codes are assigned by "intermap:*" entries in the CONFIG table.
38 ** The link is only valid if there exists an entry in the CONFIG table
39 ** that matches "intermap:Code".
40 **
41 ** Each value of each intermap:Code entry in the CONFIG table is a JSON
42 ** object with the following fields:
43 **
44 **    {
45 **      "base":  Base URL for the remote site.
46 **      "hash":  Append this to "base" for Hash targets.
47 **      "wiki":  Append this to "base" for Wiki targets.
48 **    }
49 **
50 ** If the remote wiki is Fossil, then the correct value for "hash"
51 ** is "/info/" and the correct value for "wiki" is "/wiki?name=".
52 ** If (for example) Wikipedia is the remote, then "hash" should be
53 ** omitted and the correct value for "wiki" is "/wiki/".
54 **
55 ** PageName is link name of the target wiki.  Several different forms
56 ** of PageName are recognized.
57 **
58 **    Path       If PageName is empty or begins with a "/" character, then
59 **               it is a pathname that is appended to "base".
60 **
61 **    Hash       If PageName is a hexadecimal string of 4 or more
62 **               characters, then PageName is appended to "hash" which
63 **               is then appended to "base".
64 **
65 **    Wiki       If PageName does not start with "/" and it is
66 **               not a hexadecimal string of 4 or more characters, then
67 **               PageName is appended to "wiki" and that combination is
68 **               appended to "base".
69 **
70 ** See https://en.wikipedia.org/wiki/Interwiki_links for further information
71 ** on interwiki links.
72 */
interwiki_url(const char * zTarget)73 char *interwiki_url(const char *zTarget){
74   int nCode;
75   int i;
76   const char *zPage;
77   int nPage;
78   char *zUrl = 0;
79   char *zName;
80   static Stmt q;
81   for(i=0; fossil_isalnum(zTarget[i]); i++){}
82   if( zTarget[i]!=':' ) return 0;
83   nCode = i;
84   if( nCode==4 && strncmp(zTarget,"wiki",4)==0 ) return 0;
85   zPage = zTarget + nCode + 1;
86   nPage = (int)strlen(zPage);
87   db_static_prepare(&q,
88      "SELECT json_extract(value,'$.base'),"
89            " json_extract(value,'$.hash'),"
90            " json_extract(value,'$.wiki')"
91      " FROM config WHERE name=lower($name)"
92   );
93   zName = mprintf("interwiki:%.*s", nCode, zTarget);
94   db_bind_text(&q, "$name", zName);
95   while( db_step(&q)==SQLITE_ROW ){
96     const char *zBase = db_column_text(&q,0);
97     if( zBase==0 || zBase[0]==0 ) break;
98     if( nPage==0 || zPage[0]=='/' ){
99       /* Path */
100       zUrl = mprintf("%s%s", zBase, zPage);
101     }else if( nPage>=4 && validate16(zPage,nPage) ){
102       /* Hash */
103       const char *zHash = db_column_text(&q,1);
104       if( zHash && zHash[0] ){
105         zUrl = mprintf("%s%s%s", zBase, zHash, zPage);
106       }
107     }else{
108       /* Wiki */
109       const char *zWiki = db_column_text(&q,2);
110       if( zWiki && zWiki[0] ){
111         zUrl = mprintf("%s%s%s", zBase, zWiki, zPage);
112       }
113     }
114     break;
115   }
116   db_reset(&q);
117   free(zName);
118   return zUrl;
119 }
120 
121 /*
122 ** If hyperlink target zTarget begins with an interwiki tag that ought
123 ** to be excluded from display, then return the number of characters in
124 ** that tag.
125 **
126 ** Path interwiki targets always return zero.  In other words, links
127 ** of the form:
128 **
129 **       remote:/path/to/file.txt
130 **
131 ** Do not have the interwiki tag removed.  But Hash and Wiki links are
132 ** transformed:
133 **
134 **       src:39cb0a323f2f3fb6  ->  39cb0a323f2f3fb6
135 **       fossil:To Do List     ->  To Do List
136 */
interwiki_removable_prefix(const char * zTarget)137 int interwiki_removable_prefix(const char *zTarget){
138   int i;
139   for(i=0; fossil_isalnum(zTarget[i]); i++){}
140   if( zTarget[i]!=':' ) return 0;
141   i++;
142   if( zTarget[i]==0 || zTarget[i]=='/' ) return 0;
143   return i;
144 }
145 
146 /*
147 ** Verify that a name is a valid interwiki "Code".  Rules:
148 **
149 **     *    ascii
150 **     *    alphanumeric
151 */
interwiki_valid_name(const char * zName)152 static int interwiki_valid_name(const char *zName){
153   int i;
154   for(i=0; zName[i]; i++){
155     if( !fossil_isalnum(zName[i]) ) return 0;
156   }
157   return 1;
158 }
159 
160 /*
161 ** COMMAND: interwiki*
162 **
163 ** Usage: %fossil interwiki COMMAND ...
164 **
165 ** Manage the "intermap" that defines the mapping from interwiki tags
166 ** to complete URLs for interwiki links.
167 **
168 ** >  fossil interwiki delete TAG ...
169 **
170 **        Delete one or more interwiki maps.
171 **
172 ** >  fossil interwiki edit TAG --base URL --hash PATH --wiki PATH
173 **
174 **        Create a interwiki referenced call TAG.  The base URL is
175 **        the --base option, which is required.  The --hash and --wiki
176 **        paths are optional.  The TAG must be lower-case alphanumeric
177 **        and must be unique.  A new entry is created if it does not
178 **        already exit.
179 **
180 ** >  fossil interwiki list
181 **
182 **        Show all interwiki mappings.
183 */
interwiki_cmd(void)184 void interwiki_cmd(void){
185   const char *zCmd;
186   int nCmd;
187   db_find_and_open_repository(0, 0);
188   if( g.argc<3 ){
189     usage("SUBCOMMAND ...");
190   }
191   zCmd = g.argv[2];
192   nCmd = (int)strlen(zCmd);
193   if( strncmp(zCmd,"edit",nCmd)==0 ){
194     const char *zName;
195     const char *zBase = find_option("base",0,1);
196     const char *zHash = find_option("hash",0,1);
197     const char *zWiki = find_option("wiki",0,1);
198     verify_all_options();
199     if( g.argc!=4 ) usage("add TAG ?OPTIONS?");
200     zName = g.argv[3];
201     if( zBase==0 ){
202       fossil_fatal("the --base option is required");
203     }
204     if( !interwiki_valid_name(zName) ){
205       fossil_fatal("not a valid interwiki tag: \"%s\"", zName);
206     }
207     db_begin_write();
208     db_unprotect(PROTECT_CONFIG);
209     db_multi_exec(
210        "REPLACE INTO config(name,value,mtime)"
211        " VALUES('interwiki:'||lower(%Q),"
212               " json_object('base',%Q,'hash',%Q,'wiki',%Q),"
213               " now());",
214        zName, zBase, zHash, zWiki
215     );
216     setup_incr_cfgcnt();
217     db_protect_pop();
218     db_commit_transaction();
219   }else
220   if( strncmp(zCmd, "delete", nCmd)==0 ){
221     int i;
222     verify_all_options();
223     if( g.argc<4 ) usage("delete ID ...");
224     db_begin_write();
225     db_unprotect(PROTECT_CONFIG);
226     for(i=3; i<g.argc; i++){
227       const char *zName = g.argv[i];
228       db_multi_exec(
229         "DELETE FROM config WHERE name='interwiki:%q'",
230         zName
231       );
232     }
233     setup_incr_cfgcnt();
234     db_protect_pop();
235     db_commit_transaction();
236   }else
237   if( strncmp(zCmd, "list", nCmd)==0 ){
238     Stmt q;
239     int n = 0;
240     verify_all_options();
241     db_prepare(&q,
242       "SELECT substr(name,11),"
243       "       json_extract(value,'$.base'),"
244       "       json_extract(value,'$.hash'),"
245       "       json_extract(value,'$.wiki')"
246       "  FROM config WHERE name glob 'interwiki:*'"
247     );
248     while( db_step(&q)==SQLITE_ROW ){
249       const char *zBase, *z, *zName;
250       if( n++ ) fossil_print("\n");
251       zName = db_column_text(&q,0);
252       zBase = db_column_text(&q,1);
253       fossil_print("%-15s %s\n", zName, zBase);
254       z = db_column_text(&q,2);
255       if( z ){
256         fossil_print("%15s %s%s\n", "", zBase, z);
257       }
258       z = db_column_text(&q,3);
259       if( z ){
260         fossil_print("%15s %s%s\n", "", zBase, z);
261       }
262     }
263     db_finalize(&q);
264   }else
265   {
266      fossil_fatal("unknown command \"%s\" - should be one of: "
267                   "delete edit list", zCmd);
268   }
269 }
270 
271 
272 /*
273 ** Append text to the "Markdown" or "Wiki" rules pages that shows
274 ** a table of all interwiki tags available on this system.
275 */
interwiki_append_map_table(Blob * out)276 void interwiki_append_map_table(Blob *out){
277   int n = 0;
278   Stmt q;
279   db_prepare(&q,
280     "SELECT substr(name,11), json_extract(value,'$.base')"
281     "  FROM config WHERE name glob 'interwiki:*'"
282     " ORDER BY name;"
283   );
284   while( db_step(&q)==SQLITE_ROW ){
285     if( n==0 ){
286       blob_appendf(out, "<blockquote><table>\n");
287     }
288     blob_appendf(out,"<tr><td>%h</td><td>&nbsp;&rarr;&nbsp;</td>",
289        db_column_text(&q,0));
290     blob_appendf(out,"<td>%h</td></tr>\n",
291        db_column_text(&q,1));
292     n++;
293   }
294   db_finalize(&q);
295   if( n>0 ){
296     blob_appendf(out,"</table></blockquote>\n");
297   }else{
298     blob_appendf(out,"<i>None</i></blockquote>\n");
299   }
300 }
301 
302 /*
303 ** WEBPAGE: /intermap
304 **
305 ** View and modify the interwiki tag map or "intermap".
306 ** This page is visible to administrators only.
307 */
interwiki_page(void)308 void interwiki_page(void){
309   Stmt q;
310   int n = 0;
311   const char *z;
312   const char *zTag = "";
313   const char *zBase = "";
314   const char *zHash = "";
315   const char *zWiki = "";
316   char *zErr = 0;
317 
318   login_check_credentials();
319   if( !g.perm.Read && !g.perm.RdWiki && ~g.perm.RdTkt ){
320     login_needed(0);
321     return;
322   }
323   if( g.perm.Setup && P("submit")!=0 && cgi_csrf_safe(1) ){
324     zTag = PT("tag");
325     zBase = PT("base");
326     zHash = PT("hash");
327     zWiki = PT("wiki");
328     if( zTag==0 || zTag[0]==0 || !interwiki_valid_name(zTag) ){
329       zErr = mprintf("Not a valid interwiki tag name: \"%s\"", zTag?zTag : "");
330     }else if( zBase==0 || zBase[0]==0 ){
331       db_unprotect(PROTECT_CONFIG);
332       db_multi_exec("DELETE FROM config WHERE name='interwiki:%q';", zTag);
333       db_protect_pop();
334     }else{
335       if( zHash && zHash[0]==0 ) zHash = 0;
336       if( zWiki && zWiki[0]==0 ) zWiki = 0;
337       db_unprotect(PROTECT_CONFIG);
338       db_multi_exec(
339         "REPLACE INTO config(name,value,mtime)"
340         "VALUES('interwiki:'||lower(%Q),"
341         " json_object('base',%Q,'hash',%Q,'wiki',%Q),"
342         " now());",
343         zTag, zBase, zHash, zWiki);
344       db_protect_pop();
345     }
346   }
347 
348   style_set_current_feature("interwiki");
349   style_header("Interwiki Map Configuration");
350   @ <p>Interwiki links are hyperlink targets of the form
351   @ <blockquote><i>Tag</i><b>:</b><i>PageName</i></blockquote>
352   @ <p>Such links resolve to links to <i>PageName</i> on a separate server
353   @ identified by <i>Tag</i>.  The Interwiki Map or "intermap" is a mapping
354   @ from <i>Tags</i> to complete Server URLs.
355   db_prepare(&q,
356     "SELECT substr(name,11),"
357     "       json_extract(value,'$.base'),"
358     "       json_extract(value,'$.hash'),"
359     "       json_extract(value,'$.wiki')"
360     "  FROM config WHERE name glob 'interwiki:*'"
361   );
362   while( db_step(&q)==SQLITE_ROW ){
363     if( n==0 ){
364       @ The current mapping is as follows:
365       @ <ol>
366     }
367     @ <li><p> %h(db_column_text(&q,0))
368     @ <ul>
369     @ <li> Base-URL: <tt>%h(db_column_text(&q,1))</tt>
370     z = db_column_text(&q,2);
371     if( z==0 ){
372       @ <li> Hash-path: <i>NULL</i>
373     }else{
374       @ <li> Hash-path: <tt>%h(z)</tt>
375     }
376     z = db_column_text(&q,3);
377     if( z==0 ){
378       @ <li> Wiki-path: <i>NULL</i>
379     }else{
380       @ <li> Wiki-path: <tt>%h(z)</tt>
381     }
382     @ </ul>
383     n++;
384   }
385   db_finalize(&q);
386   if( n ){
387     @ </ol>
388   }else{
389     @ No mappings are currently defined.
390   }
391 
392   if( !g.perm.Setup ){
393     /* Do not show intermap editing fields to non-setup users */
394     style_finish_page();
395     return;
396   }
397 
398   @ <p>To add a new mapping, fill out the form below providing a unique name
399   @ for the tag.  To edit an exist mapping, fill out the form and use the
400   @ existing name as the tag.  To delete an existing mapping, fill in the
401   @ tag field but leave the "Base URL" field blank.</p>
402   if( zErr ){
403     @ <p class="error">%h(zErr)</p>
404   }
405   @ <form method="POST" action="%R/intermap">
406   login_insert_csrf_secret();
407   @ <table border="0">
408   @ <tr><td class="form_label" id="imtag">Tag:</td>
409   @ <td><input type="text" id="tag" aria-labeledby="imtag" name="tag" \
410   @ size="15" value="%h(zTag)"></td></tr>
411   @ <tr><td class="form_label" id="imbase">Base&nbsp;URL:</td>
412   @ <td><input type="text" id="base" aria-labeledby="imbase" name="base" \
413   @ size="70" value="%h(zBase)"></td></tr>
414   @ <tr><td class="form_label" id="imhash">Hash-path:</td>
415   @ <td><input type="text" id="hash" aria-labeledby="imhash" name="hash" \
416   @ size="20" value="%h(zHash)">
417   @ (use "<tt>/info/</tt>" when the target is Fossil)</td></tr>
418   @ <tr><td class="form_label" id="imwiki">Wiki-path:</td>
419   @ <td><input type="text" id="wiki" aria-labeledby="imwiki" name="wiki" \
420   @ size="20" value="%h(zWiki)">
421   @ (use "<tt>/wiki?name=</tt>" when the target is Fossil)</td></tr>
422   @ <tr><td></td>
423   @ <td><input type="submit" name="submit" value="Apply Changes"></td></tr>
424   @ </table>
425   @ </form>
426 
427   style_finish_page();
428 }
429