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> → </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 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