Fossil SCM

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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button