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