|
1
|
/* |
|
2
|
** Copyright (c) 2007 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 code to implement the "/doc" web page and related |
|
19
|
** pages. |
|
20
|
*/ |
|
21
|
#include "config.h" |
|
22
|
#include "doc.h" |
|
23
|
#include <assert.h> |
|
24
|
|
|
25
|
/* |
|
26
|
** Try to guess the mimetype from content. |
|
27
|
** |
|
28
|
** If the content is pure text, return NULL. |
|
29
|
** |
|
30
|
** For image types, attempt to return an appropriate mimetype |
|
31
|
** name like "image/gif" or "image/jpeg". |
|
32
|
** |
|
33
|
** For any other binary type, return "unknown/unknown". |
|
34
|
*/ |
|
35
|
const char *mimetype_from_content(Blob *pBlob){ |
|
36
|
int i; |
|
37
|
int n; |
|
38
|
const unsigned char *x; |
|
39
|
|
|
40
|
/* A table of mimetypes based on file content prefixes |
|
41
|
*/ |
|
42
|
static const struct { |
|
43
|
const char *z; /* Identifying file text */ |
|
44
|
const unsigned char sz1; /* Length of the prefix */ |
|
45
|
const unsigned char of2; /* Offset to the second segment */ |
|
46
|
const unsigned char sz2; /* Size of the second segment */ |
|
47
|
const unsigned char mn; /* Minimum size of input */ |
|
48
|
const char *zMimetype; /* The corresponding mimetype */ |
|
49
|
} aMime[] = { |
|
50
|
{ "GIF87a", 6, 0, 0, 6, "image/gif" }, |
|
51
|
{ "GIF89a", 6, 0, 0, 6, "image/gif" }, |
|
52
|
{ "\211PNG\r\n\032\n", 8, 0, 0, 8, "image/png" }, |
|
53
|
{ "\377\332\377", 3, 0, 0, 3, "image/jpeg" }, |
|
54
|
{ "\377\330\377", 3, 0, 0, 3, "image/jpeg" }, |
|
55
|
{ "RIFFWAVEfmt", 4, 8, 7, 15, "sound/wav" }, |
|
56
|
}; |
|
57
|
|
|
58
|
if( !looks_like_binary(pBlob) ) { |
|
59
|
return 0; /* Plain text */ |
|
60
|
} |
|
61
|
x = (const unsigned char*)blob_buffer(pBlob); |
|
62
|
n = blob_size(pBlob); |
|
63
|
for(i=0; i<count(aMime); i++){ |
|
64
|
if( n<aMime[i].mn ) continue; |
|
65
|
if( memcmp(x, aMime[i].z, aMime[i].sz1)!=0 ) continue; |
|
66
|
if( aMime[i].sz2 |
|
67
|
&& memcmp(x+aMime[i].of2, aMime[i].z+aMime[i].sz1, aMime[i].sz2)!=0 |
|
68
|
){ |
|
69
|
continue; |
|
70
|
} |
|
71
|
return aMime[i].zMimetype; |
|
72
|
} |
|
73
|
return "unknown/unknown"; |
|
74
|
} |
|
75
|
|
|
76
|
/* A table of mimetypes based on file suffixes. |
|
77
|
** Suffixes must be in sorted order so that we can do a binary |
|
78
|
** search to find the mimetype. |
|
79
|
*/ |
|
80
|
static const struct { |
|
81
|
const char *zSuffix; /* The file suffix */ |
|
82
|
int size; /* Length of the suffix */ |
|
83
|
const char *zMimetype; /* The corresponding mimetype */ |
|
84
|
} aMime[] = { |
|
85
|
{ "ai", 2, "application/postscript" }, |
|
86
|
{ "aif", 3, "audio/x-aiff" }, |
|
87
|
{ "aifc", 4, "audio/x-aiff" }, |
|
88
|
{ "aiff", 4, "audio/x-aiff" }, |
|
89
|
{ "arj", 3, "application/x-arj-compressed" }, |
|
90
|
{ "asc", 3, "text/plain" }, |
|
91
|
{ "asf", 3, "video/x-ms-asf" }, |
|
92
|
{ "asx", 3, "video/x-ms-asx" }, |
|
93
|
{ "au", 2, "audio/ulaw" }, |
|
94
|
{ "avi", 3, "video/x-msvideo" }, |
|
95
|
{ "bat", 3, "application/x-msdos-program" }, |
|
96
|
{ "bcpio", 5, "application/x-bcpio" }, |
|
97
|
{ "bin", 3, "application/octet-stream" }, |
|
98
|
{ "bmp", 3, "image/bmp" }, |
|
99
|
{ "bz2", 3, "application/x-bzip2" }, |
|
100
|
{ "bzip", 4, "application/x-bzip" }, |
|
101
|
{ "c", 1, "text/plain" }, |
|
102
|
{ "cc", 2, "text/plain" }, |
|
103
|
{ "ccad", 4, "application/clariscad" }, |
|
104
|
{ "cdf", 3, "application/x-netcdf" }, |
|
105
|
{ "class", 5, "application/octet-stream" }, |
|
106
|
{ "cod", 3, "application/vnd.rim.cod" }, |
|
107
|
{ "com", 3, "application/x-msdos-program" }, |
|
108
|
{ "cpio", 4, "application/x-cpio" }, |
|
109
|
{ "cpt", 3, "application/mac-compactpro" }, |
|
110
|
{ "cs", 2, "text/plain" }, |
|
111
|
{ "csh", 3, "application/x-csh" }, |
|
112
|
{ "css", 3, "text/css" }, |
|
113
|
{ "csv", 3, "text/csv" }, |
|
114
|
{ "dcr", 3, "application/x-director" }, |
|
115
|
{ "deb", 3, "application/x-debian-package" }, |
|
116
|
{ "dib", 3, "image/bmp" }, |
|
117
|
{ "dir", 3, "application/x-director" }, |
|
118
|
{ "dl", 2, "video/dl" }, |
|
119
|
{ "dms", 3, "application/octet-stream" }, |
|
120
|
{ "doc", 3, "application/msword" }, |
|
121
|
{ "docx", 4, "application/vnd.openxmlformats-" |
|
122
|
"officedocument.wordprocessingml.document"}, |
|
123
|
{ "dot", 3, "application/msword" }, |
|
124
|
{ "dotx", 4, "application/vnd.openxmlformats-" |
|
125
|
"officedocument.wordprocessingml.template"}, |
|
126
|
{ "drw", 3, "application/drafting" }, |
|
127
|
{ "dvi", 3, "application/x-dvi" }, |
|
128
|
{ "dwg", 3, "application/acad" }, |
|
129
|
{ "dxf", 3, "application/dxf" }, |
|
130
|
{ "dxr", 3, "application/x-director" }, |
|
131
|
{ "eps", 3, "application/postscript" }, |
|
132
|
{ "etx", 3, "text/x-setext" }, |
|
133
|
{ "exe", 3, "application/octet-stream" }, |
|
134
|
{ "ez", 2, "application/andrew-inset" }, |
|
135
|
{ "f", 1, "text/plain" }, |
|
136
|
{ "f90", 3, "text/plain" }, |
|
137
|
{ "fli", 3, "video/fli" }, |
|
138
|
{ "flv", 3, "video/flv" }, |
|
139
|
{ "gif", 3, "image/gif" }, |
|
140
|
{ "gl", 2, "video/gl" }, |
|
141
|
{ "gtar", 4, "application/x-gtar" }, |
|
142
|
{ "gz", 2, "application/x-gzip" }, |
|
143
|
{ "h", 1, "text/plain" }, |
|
144
|
{ "hdf", 3, "application/x-hdf" }, |
|
145
|
{ "hh", 2, "text/plain" }, |
|
146
|
{ "hqx", 3, "application/mac-binhex40" }, |
|
147
|
{ "htm", 3, "text/html" }, |
|
148
|
{ "html", 4, "text/html" }, |
|
149
|
{ "ice", 3, "x-conference/x-cooltalk" }, |
|
150
|
{ "ico", 3, "image/vnd.microsoft.icon" }, |
|
151
|
{ "ief", 3, "image/ief" }, |
|
152
|
{ "iges", 4, "model/iges" }, |
|
153
|
{ "igs", 3, "model/iges" }, |
|
154
|
{ "ips", 3, "application/x-ipscript" }, |
|
155
|
{ "ipx", 3, "application/x-ipix" }, |
|
156
|
{ "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, |
|
157
|
{ "jar", 3, "application/java-archive" }, |
|
158
|
{ "jp2", 3, "image/jp2" }, |
|
159
|
{ "jpe", 3, "image/jpeg" }, |
|
160
|
{ "jpeg", 4, "image/jpeg" }, |
|
161
|
{ "jpg", 3, "image/jpeg" }, |
|
162
|
{ "js", 2, "text/javascript" }, |
|
163
|
/* application/javascript is commonly used for JS, but the |
|
164
|
** spec says text/javascript is correct: |
|
165
|
** https://html.spec.whatwg.org/multipage/scripting.html |
|
166
|
** #scriptingLanguages:javascript-mime-type */ |
|
167
|
{ "json", 4, "application/json" }, |
|
168
|
{ "kar", 3, "audio/midi" }, |
|
169
|
{ "latex", 5, "application/x-latex" }, |
|
170
|
{ "lha", 3, "application/octet-stream" }, |
|
171
|
{ "lsp", 3, "application/x-lisp" }, |
|
172
|
{ "lzh", 3, "application/octet-stream" }, |
|
173
|
{ "m", 1, "text/plain" }, |
|
174
|
{ "m3u", 3, "audio/x-mpegurl" }, |
|
175
|
{ "man", 3, "text/plain" }, |
|
176
|
{ "markdown", 8, "text/x-markdown" }, |
|
177
|
{ "md", 2, "text/x-markdown" }, |
|
178
|
{ "me", 2, "application/x-troff-me" }, |
|
179
|
{ "mesh", 4, "model/mesh" }, |
|
180
|
{ "mid", 3, "audio/midi" }, |
|
181
|
{ "midi", 4, "audio/midi" }, |
|
182
|
{ "mif", 3, "application/x-mif" }, |
|
183
|
{ "mime", 4, "www/mime" }, |
|
184
|
{ "mjs", 3, "text/javascript" /*ES6 module*/ }, |
|
185
|
{ "mkd", 3, "text/x-markdown" }, |
|
186
|
{ "mov", 3, "video/quicktime" }, |
|
187
|
{ "movie", 5, "video/x-sgi-movie" }, |
|
188
|
{ "mp2", 3, "audio/mpeg" }, |
|
189
|
{ "mp3", 3, "audio/mpeg" }, |
|
190
|
{ "mp4", 3, "video/mp4" }, |
|
191
|
{ "mpe", 3, "video/mpeg" }, |
|
192
|
{ "mpeg", 4, "video/mpeg" }, |
|
193
|
{ "mpg", 3, "video/mpeg" }, |
|
194
|
{ "mpga", 4, "audio/mpeg" }, |
|
195
|
{ "ms", 2, "application/x-troff-ms" }, |
|
196
|
{ "msh", 3, "model/mesh" }, |
|
197
|
{ "n", 1, "text/plain" }, |
|
198
|
{ "nc", 2, "application/x-netcdf" }, |
|
199
|
{ "oda", 3, "application/oda" }, |
|
200
|
{ "odp", 3, "application/vnd.oasis.opendocument.presentation" }, |
|
201
|
{ "ods", 3, "application/vnd.oasis.opendocument.spreadsheet" }, |
|
202
|
{ "odt", 3, "application/vnd.oasis.opendocument.text" }, |
|
203
|
{ "ogg", 3, "application/ogg" }, |
|
204
|
{ "ogm", 3, "application/ogg" }, |
|
205
|
{ "otf", 3, "font/otf" }, |
|
206
|
{ "pbm", 3, "image/x-portable-bitmap" }, |
|
207
|
{ "pdb", 3, "chemical/x-pdb" }, |
|
208
|
{ "pdf", 3, "application/pdf" }, |
|
209
|
{ "pgm", 3, "image/x-portable-graymap" }, |
|
210
|
{ "pgn", 3, "application/x-chess-pgn" }, |
|
211
|
{ "pgp", 3, "application/pgp" }, |
|
212
|
{ "pikchr", 6, "text/x-pikchr" }, |
|
213
|
{ "pl", 2, "application/x-perl" }, |
|
214
|
{ "pm", 2, "application/x-perl" }, |
|
215
|
{ "png", 3, "image/png" }, |
|
216
|
{ "pnm", 3, "image/x-portable-anymap" }, |
|
217
|
{ "pot", 3, "application/mspowerpoint" }, |
|
218
|
{ "potx", 4, "application/vnd.openxmlformats-" |
|
219
|
"officedocument.presentationml.template"}, |
|
220
|
{ "ppm", 3, "image/x-portable-pixmap" }, |
|
221
|
{ "pps", 3, "application/mspowerpoint" }, |
|
222
|
{ "ppsx", 4, "application/vnd.openxmlformats-" |
|
223
|
"officedocument.presentationml.slideshow"}, |
|
224
|
{ "ppt", 3, "application/mspowerpoint" }, |
|
225
|
{ "pptx", 4, "application/vnd.openxmlformats-" |
|
226
|
"officedocument.presentationml.presentation"}, |
|
227
|
{ "ppz", 3, "application/mspowerpoint" }, |
|
228
|
{ "pre", 3, "application/x-freelance" }, |
|
229
|
{ "prt", 3, "application/pro_eng" }, |
|
230
|
{ "ps", 2, "application/postscript" }, |
|
231
|
{ "qt", 2, "video/quicktime" }, |
|
232
|
{ "ra", 2, "audio/x-realaudio" }, |
|
233
|
{ "ram", 3, "audio/x-pn-realaudio" }, |
|
234
|
{ "rar", 3, "application/x-rar-compressed" }, |
|
235
|
{ "ras", 3, "image/cmu-raster" }, |
|
236
|
{ "rgb", 3, "image/x-rgb" }, |
|
237
|
{ "rm", 2, "audio/x-pn-realaudio" }, |
|
238
|
{ "roff", 4, "application/x-troff" }, |
|
239
|
{ "rpm", 3, "audio/x-pn-realaudio-plugin" }, |
|
240
|
{ "rtf", 3, "text/rtf" }, |
|
241
|
{ "rtx", 3, "text/richtext" }, |
|
242
|
{ "scm", 3, "application/x-lotusscreencam" }, |
|
243
|
{ "set", 3, "application/set" }, |
|
244
|
{ "sgm", 3, "text/sgml" }, |
|
245
|
{ "sgml", 4, "text/sgml" }, |
|
246
|
{ "sh", 2, "application/x-sh" }, |
|
247
|
{ "shar", 4, "application/x-shar" }, |
|
248
|
{ "silo", 4, "model/mesh" }, |
|
249
|
{ "sit", 3, "application/x-stuffit" }, |
|
250
|
{ "skd", 3, "application/x-koan" }, |
|
251
|
{ "skm", 3, "application/x-koan" }, |
|
252
|
{ "skp", 3, "application/x-koan" }, |
|
253
|
{ "skt", 3, "application/x-koan" }, |
|
254
|
{ "smi", 3, "application/smil" }, |
|
255
|
{ "smil", 4, "application/smil" }, |
|
256
|
{ "snd", 3, "audio/basic" }, |
|
257
|
{ "sol", 3, "application/solids" }, |
|
258
|
{ "spl", 3, "application/x-futuresplash" }, |
|
259
|
{ "sql", 3, "application/sql" }, |
|
260
|
{ "src", 3, "application/x-wais-source" }, |
|
261
|
{ "step", 4, "application/STEP" }, |
|
262
|
{ "stl", 3, "application/SLA" }, |
|
263
|
{ "stp", 3, "application/STEP" }, |
|
264
|
{ "sv4cpio", 7, "application/x-sv4cpio" }, |
|
265
|
{ "sv4crc", 6, "application/x-sv4crc" }, |
|
266
|
{ "svg", 3, "image/svg+xml" }, |
|
267
|
{ "swf", 3, "application/x-shockwave-flash" }, |
|
268
|
{ "t", 1, "application/x-troff" }, |
|
269
|
{ "tar", 3, "application/x-tar" }, |
|
270
|
{ "tcl", 3, "application/x-tcl" }, |
|
271
|
{ "tex", 3, "application/x-tex" }, |
|
272
|
{ "texi", 4, "application/x-texinfo" }, |
|
273
|
{ "texinfo", 7, "application/x-texinfo" }, |
|
274
|
{ "tgz", 3, "application/x-tar-gz" }, |
|
275
|
{ "th1", 3, "application/x-th1" }, |
|
276
|
{ "tif", 3, "image/tiff" }, |
|
277
|
{ "tiff", 4, "image/tiff" }, |
|
278
|
{ "tr", 2, "application/x-troff" }, |
|
279
|
{ "tsi", 3, "audio/TSP-audio" }, |
|
280
|
{ "tsp", 3, "application/dsptype" }, |
|
281
|
{ "tsv", 3, "text/tab-separated-values" }, |
|
282
|
{ "txt", 3, "text/plain" }, |
|
283
|
{ "unv", 3, "application/i-deas" }, |
|
284
|
{ "ustar", 5, "application/x-ustar" }, |
|
285
|
{ "vb", 2, "text/plain" }, |
|
286
|
{ "vcd", 3, "application/x-cdlink" }, |
|
287
|
{ "vda", 3, "application/vda" }, |
|
288
|
{ "viv", 3, "video/vnd.vivo" }, |
|
289
|
{ "vivo", 4, "video/vnd.vivo" }, |
|
290
|
{ "vrml", 4, "model/vrml" }, |
|
291
|
{ "wasm", 4, "application/wasm" }, |
|
292
|
{ "wav", 3, "audio/x-wav" }, |
|
293
|
{ "wax", 3, "audio/x-ms-wax" }, |
|
294
|
{ "webp", 4, "image/webp" }, |
|
295
|
{ "wiki", 4, "text/x-fossil-wiki" }, |
|
296
|
{ "wma", 3, "audio/x-ms-wma" }, |
|
297
|
{ "wmv", 3, "video/x-ms-wmv" }, |
|
298
|
{ "wmx", 3, "video/x-ms-wmx" }, |
|
299
|
{ "wrl", 3, "model/vrml" }, |
|
300
|
{ "wvx", 3, "video/x-ms-wvx" }, |
|
301
|
{ "xbm", 3, "image/x-xbitmap" }, |
|
302
|
{ "xlc", 3, "application/vnd.ms-excel" }, |
|
303
|
{ "xll", 3, "application/vnd.ms-excel" }, |
|
304
|
{ "xlm", 3, "application/vnd.ms-excel" }, |
|
305
|
{ "xls", 3, "application/vnd.ms-excel" }, |
|
306
|
{ "xlsx", 4, "application/vnd.openxmlformats-" |
|
307
|
"officedocument.spreadsheetml.sheet"}, |
|
308
|
{ "xlw", 3, "application/vnd.ms-excel" }, |
|
309
|
{ "xml", 3, "text/xml" }, |
|
310
|
{ "xpm", 3, "image/x-xpixmap" }, |
|
311
|
{ "xsl", 3, "text/xml" }, |
|
312
|
{ "xslt", 4, "text/xml" }, |
|
313
|
{ "xwd", 3, "image/x-xwindowdump" }, |
|
314
|
{ "xyz", 3, "chemical/x-pdb" }, |
|
315
|
{ "zip", 3, "application/zip" }, |
|
316
|
}; |
|
317
|
|
|
318
|
/* |
|
319
|
** Verify that all entries in the aMime[] table are in sorted order. |
|
320
|
** Abort with a fatal error if any is out-of-order. |
|
321
|
*/ |
|
322
|
static void mimetype_verify(void){ |
|
323
|
int i; |
|
324
|
for(i=1; i<count(aMime); i++){ |
|
325
|
if( fossil_strcmp(aMime[i-1].zSuffix,aMime[i].zSuffix)>=0 ){ |
|
326
|
fossil_panic("mimetypes out of sequence: %s before %s", |
|
327
|
aMime[i-1].zSuffix, aMime[i].zSuffix); |
|
328
|
} |
|
329
|
} |
|
330
|
} |
|
331
|
|
|
332
|
/* |
|
333
|
** Looks in the contents of the "mimetypes" setting for a suffix |
|
334
|
** matching zSuffix. If found, it returns the configured value |
|
335
|
** in memory owned by the app (i.e. do not free() it), else it |
|
336
|
** returns 0. |
|
337
|
** |
|
338
|
** The mimetypes setting is expected to be a list of file extensions |
|
339
|
** and mimetypes, with one such mapping per line. A leading '.' on |
|
340
|
** extensions is permitted for compatibility with lists imported from |
|
341
|
** other tools which require them. |
|
342
|
*/ |
|
343
|
static const char *mimetype_from_name_custom(const char *zSuffix){ |
|
344
|
static char * zList = 0; |
|
345
|
static char const * zEnd = 0; |
|
346
|
static int once = 0; |
|
347
|
char * z; |
|
348
|
int tokenizerState /* 0=expecting a key, 1=skip next token, |
|
349
|
** 2=accept next token */; |
|
350
|
if(once==0){ |
|
351
|
once = 1; |
|
352
|
zList = db_get("mimetypes",0); |
|
353
|
if(zList==0){ |
|
354
|
return 0; |
|
355
|
} |
|
356
|
/* Transform zList to simplify the main loop: |
|
357
|
replace non-newline spaces with NUL bytes. */ |
|
358
|
zEnd = zList + strlen(zList); |
|
359
|
for(z = zList; z<zEnd; ++z){ |
|
360
|
if('\n'==*z) continue; |
|
361
|
else if(fossil_isspace(*z)){ |
|
362
|
*z = 0; |
|
363
|
} |
|
364
|
} |
|
365
|
}else if(zList==0){ |
|
366
|
return 0; |
|
367
|
} |
|
368
|
tokenizerState = 0; |
|
369
|
z = zList; |
|
370
|
while( z<zEnd ){ |
|
371
|
if(*z==0){ |
|
372
|
++z; |
|
373
|
continue; |
|
374
|
} |
|
375
|
else if('\n'==*z){ |
|
376
|
if(2==tokenizerState){ |
|
377
|
/* We were expecting a value for a successful match |
|
378
|
here, but got no value. Bail out. */ |
|
379
|
break; |
|
380
|
}else{ |
|
381
|
/* May happen on malformed inputs. Skip this record. */ |
|
382
|
tokenizerState = 0; |
|
383
|
++z; |
|
384
|
continue; |
|
385
|
} |
|
386
|
} |
|
387
|
switch(tokenizerState){ |
|
388
|
case 0:{ /* This is a file extension */ |
|
389
|
static char * zCase = 0; |
|
390
|
if('.'==*z){ |
|
391
|
/*ignore an optional leading dot, for compatibility |
|
392
|
with some external mimetype lists*/; |
|
393
|
if(++z==zEnd){ |
|
394
|
break; |
|
395
|
} |
|
396
|
} |
|
397
|
if(zCase<z){ |
|
398
|
/*we have not yet case-folded this section: lower-case it*/ |
|
399
|
for(zCase = z; zCase<zEnd && *zCase!=0; ++zCase){ |
|
400
|
if(!(0x80 & *zCase)){ |
|
401
|
*zCase = (char)fossil_tolower(*zCase); |
|
402
|
} |
|
403
|
} |
|
404
|
} |
|
405
|
if(strcmp(z,zSuffix)==0){ |
|
406
|
tokenizerState = 2 /* Match: accept the next value. */; |
|
407
|
}else{ |
|
408
|
tokenizerState = 1 /* No match: skip the next value. */; |
|
409
|
} |
|
410
|
z += strlen(z); |
|
411
|
break; |
|
412
|
} |
|
413
|
case 1: /* This is a value, but not a match. Skip it. */ |
|
414
|
z += strlen(z); |
|
415
|
break; |
|
416
|
case 2: /* This is the value which matched the previous key. */; |
|
417
|
return z; |
|
418
|
default: |
|
419
|
assert(!"cannot happen - invalid tokenizerState value."); |
|
420
|
} |
|
421
|
} |
|
422
|
return 0; |
|
423
|
} |
|
424
|
|
|
425
|
/* |
|
426
|
** Emit Javascript which applies (or optionally can apply) to both the |
|
427
|
** /doc and /wiki pages. None of this implements required |
|
428
|
** functionality, just nice-to-haves. Any calls after the first are |
|
429
|
** no-ops. |
|
430
|
*/ |
|
431
|
void document_emit_js(void){ |
|
432
|
static int once = 0; |
|
433
|
if(0==once++){ |
|
434
|
builtin_fossil_js_bundle_or("pikchr", NULL); |
|
435
|
style_script_begin(__FILE__,__LINE__); |
|
436
|
CX("window.addEventListener('load', " |
|
437
|
"()=>window.fossil.pikchr.addSrcView(), " |
|
438
|
"false);\n"); |
|
439
|
style_script_end(); |
|
440
|
} |
|
441
|
} |
|
442
|
|
|
443
|
/* |
|
444
|
** Guess the mimetype of a document based on its name. |
|
445
|
*/ |
|
446
|
const char *mimetype_from_name(const char *zName){ |
|
447
|
const char *z; |
|
448
|
int i; |
|
449
|
int first, last; |
|
450
|
int len; |
|
451
|
char zSuffix[20]; |
|
452
|
|
|
453
|
|
|
454
|
#ifdef FOSSIL_DEBUG |
|
455
|
/* This is test code to make sure the table above is in the correct |
|
456
|
** order |
|
457
|
*/ |
|
458
|
if( fossil_strcmp(zName, "mimetype-test")==0 ){ |
|
459
|
mimetype_verify(); |
|
460
|
return "ok"; |
|
461
|
} |
|
462
|
#endif |
|
463
|
|
|
464
|
z = zName; |
|
465
|
for(i=0; zName[i]; i++){ |
|
466
|
if( zName[i]=='.' ) z = &zName[i+1]; |
|
467
|
} |
|
468
|
len = strlen(z); |
|
469
|
if( len<(int)sizeof(zSuffix)-1 ){ |
|
470
|
sqlite3_snprintf(sizeof(zSuffix), zSuffix, "%s", z); |
|
471
|
for(i=0; zSuffix[i]; i++) zSuffix[i] = fossil_tolower(zSuffix[i]); |
|
472
|
z = mimetype_from_name_custom(zSuffix); |
|
473
|
if(z!=0){ |
|
474
|
return z; |
|
475
|
} |
|
476
|
first = 0; |
|
477
|
last = count(aMime) - 1; |
|
478
|
while( first<=last ){ |
|
479
|
int c; |
|
480
|
i = (first+last)/2; |
|
481
|
c = fossil_strcmp(zSuffix, aMime[i].zSuffix); |
|
482
|
if( c==0 ) return aMime[i].zMimetype; |
|
483
|
if( c<0 ){ |
|
484
|
last = i-1; |
|
485
|
}else{ |
|
486
|
first = i+1; |
|
487
|
} |
|
488
|
} |
|
489
|
} |
|
490
|
return "application/x-fossil-artifact"; |
|
491
|
} |
|
492
|
|
|
493
|
/* |
|
494
|
** COMMAND: test-mimetype |
|
495
|
** |
|
496
|
** Usage: %fossil test-mimetype FILENAME... |
|
497
|
** |
|
498
|
** Return the deduced mimetype for each file listed. |
|
499
|
** |
|
500
|
** If Fossil is compiled with -DFOSSIL_DEBUG then the "mimetype-test" |
|
501
|
** filename is special and verifies the integrity of the mimetype table. |
|
502
|
** It should return "ok". |
|
503
|
*/ |
|
504
|
void mimetype_test_cmd(void){ |
|
505
|
int i; |
|
506
|
mimetype_verify(); |
|
507
|
db_find_and_open_repository(0, 0); |
|
508
|
for(i=2; i<g.argc; i++){ |
|
509
|
fossil_print("%-20s -> %s\n", g.argv[i], mimetype_from_name(g.argv[i])); |
|
510
|
} |
|
511
|
} |
|
512
|
|
|
513
|
/* |
|
514
|
** WEBPAGE: mimetype_list |
|
515
|
** |
|
516
|
** Show the built-in table used to guess embedded document mimetypes |
|
517
|
** from file suffixes. |
|
518
|
*/ |
|
519
|
void mimetype_list_page(void){ |
|
520
|
int i; |
|
521
|
char *zCustomList = 0; /* value of the mimetypes setting */ |
|
522
|
int nCustomEntries = 0; /* number of entries in the mimetypes |
|
523
|
** setting */ |
|
524
|
mimetype_verify(); |
|
525
|
style_header("Mimetype List"); |
|
526
|
@ <p>The Fossil <a href="%R/help/www/doc">/doc</a> page uses filename |
|
527
|
@ suffixes and the following tables to guess at the appropriate mimetype |
|
528
|
@ for each document. Mimetypes may be customized and overridden using |
|
529
|
@ <a href="%R/help/mimetypes">the mimetypes config setting</a>.</p> |
|
530
|
zCustomList = db_get("mimetypes",0); |
|
531
|
if( zCustomList!=0 ){ |
|
532
|
Blob list, entry, key, val; |
|
533
|
@ <h1>Repository-specific mimetypes</h1> |
|
534
|
@ <p>The following extension-to-mimetype mappings are defined via |
|
535
|
@ the <a href="%R/help/mimetypes">mimetypes setting</a>.</p> |
|
536
|
@ <table class='sortable mimetypetable' border=1 cellpadding=0 \ |
|
537
|
@ data-column-types='tt' data-init-sort='0'> |
|
538
|
@ <thead> |
|
539
|
@ <tr><th>Suffix<th>Mimetype |
|
540
|
@ </thead> |
|
541
|
@ <tbody> |
|
542
|
blob_set(&list, zCustomList); |
|
543
|
while( blob_line(&list, &entry)>0 ){ |
|
544
|
const char *zKey; |
|
545
|
if( blob_token(&entry, &key)==0 ) continue; |
|
546
|
if( blob_token(&entry, &val)==0 ) continue; |
|
547
|
zKey = blob_str(&key); |
|
548
|
if( zKey[0]=='.' ) zKey++; |
|
549
|
@ <tr><td>%h(zKey)<td>%h(blob_str(&val))</tr> |
|
550
|
nCustomEntries++; |
|
551
|
} |
|
552
|
fossil_free(zCustomList); |
|
553
|
if( nCustomEntries==0 ){ |
|
554
|
/* This can happen if the option is set to an empty/space-only |
|
555
|
** value. */ |
|
556
|
@ <tr><td colspan="2"><em>none</em></tr> |
|
557
|
} |
|
558
|
@ </tbody></table> |
|
559
|
} |
|
560
|
@ <h1>Default built-in mimetypes</h1> |
|
561
|
if(nCustomEntries>0){ |
|
562
|
@ <p>Entries starting with an exclamation mark <em><strong>!</strong></em> |
|
563
|
@ are overwritten by repository-specific settings.</p> |
|
564
|
} |
|
565
|
@ <table class='sortable mimetypetable' border=1 cellpadding=0 \ |
|
566
|
@ data-column-types='tt' data-init-sort='1'> |
|
567
|
@ <thead> |
|
568
|
@ <tr><th>Suffix<th>Mimetype |
|
569
|
@ </thead> |
|
570
|
@ <tbody> |
|
571
|
for(i=0; i<count(aMime); i++){ |
|
572
|
const char *zFlag = ""; |
|
573
|
if(nCustomEntries>0 && |
|
574
|
mimetype_from_name_custom(aMime[i].zSuffix)!=0){ |
|
575
|
zFlag = "<em><strong>!</strong></em> "; |
|
576
|
} |
|
577
|
@ <tr><td>%s(zFlag)%h(aMime[i].zSuffix)<td>%h(aMime[i].zMimetype)</tr> |
|
578
|
} |
|
579
|
@ </tbody></table> |
|
580
|
style_table_sorter(); |
|
581
|
style_finish_page(); |
|
582
|
} |
|
583
|
|
|
584
|
/* |
|
585
|
** Check to see if the file in the pContent blob is "embedded HTML". Return |
|
586
|
** true if it is, and fill pTitle with the document title. |
|
587
|
** |
|
588
|
** An "embedded HTML" file is HTML that lacks a header and a footer. The |
|
589
|
** standard Fossil header is prepended and the standard Fossil footer is |
|
590
|
** appended. Otherwise, the file is displayed without change. |
|
591
|
** |
|
592
|
** Embedded HTML must be contained in a <div class='fossil-doc'> element. |
|
593
|
** If that <div> also contains a data-title attribute, then the |
|
594
|
** value of that attribute is extracted into pTitle and becomes the title |
|
595
|
** of the document. |
|
596
|
*/ |
|
597
|
int doc_is_embedded_html(Blob *pContent, Blob *pTitle){ |
|
598
|
const char *zIn = blob_str(pContent); |
|
599
|
const char *zAttr; |
|
600
|
const char *zValue; |
|
601
|
int nAttr, nValue; |
|
602
|
int seenClass = 0; |
|
603
|
int seenTitle = 0; |
|
604
|
|
|
605
|
while( fossil_isspace(zIn[0]) ) zIn++; |
|
606
|
if( fossil_strnicmp(zIn,"<div",4)!=0 ) return 0; |
|
607
|
zIn += 4; |
|
608
|
while( zIn[0] ){ |
|
609
|
if( fossil_isspace(zIn[0]) ) zIn++; |
|
610
|
if( zIn[0]=='>' ) break; |
|
611
|
zAttr = zIn; |
|
612
|
while( fossil_isalnum(zIn[0]) || zIn[0]=='-' ) zIn++; |
|
613
|
nAttr = (int)(zIn - zAttr); |
|
614
|
while( fossil_isspace(zIn[0]) ) zIn++; |
|
615
|
if( zIn[0]!='=' ) continue; |
|
616
|
zIn++; |
|
617
|
while( fossil_isspace(zIn[0]) ) zIn++; |
|
618
|
if( zIn[0]=='"' || zIn[0]=='\'' ){ |
|
619
|
char cDelim = zIn[0]; |
|
620
|
zIn++; |
|
621
|
zValue = zIn; |
|
622
|
while( zIn[0] && zIn[0]!=cDelim ) zIn++; |
|
623
|
if( zIn[0]==0 ) return 0; |
|
624
|
nValue = (int)(zIn - zValue); |
|
625
|
zIn++; |
|
626
|
}else{ |
|
627
|
zValue = zIn; |
|
628
|
while( zIn[0]!=0 && zIn[0]!='>' && zIn[0]!='/' |
|
629
|
&& !fossil_isspace(zIn[0]) ) zIn++; |
|
630
|
if( zIn[0]==0 ) return 0; |
|
631
|
nValue = (int)(zIn - zValue); |
|
632
|
} |
|
633
|
if( nAttr==5 && fossil_strnicmp(zAttr,"class",5)==0 ){ |
|
634
|
if( nValue!=10 || fossil_strnicmp(zValue,"fossil-doc",10)!=0 ) return 0; |
|
635
|
seenClass = 1; |
|
636
|
if( seenTitle ) return 1; |
|
637
|
} |
|
638
|
if( nAttr==10 && fossil_strnicmp(zAttr,"data-title",10)==0 ){ |
|
639
|
/* The text argument to data-title="" will have had any characters that |
|
640
|
** are special to HTML encoded. We need to decode these before turning |
|
641
|
** the text into a title, as the title text will be reencoded later */ |
|
642
|
char *zTitle = mprintf("%.*s", nValue, zValue); |
|
643
|
int i; |
|
644
|
for(i=0; fossil_isspace(zTitle[i]); i++){} |
|
645
|
html_to_plaintext(zTitle+i, pTitle, 0); |
|
646
|
fossil_free(zTitle); |
|
647
|
seenTitle = 1; |
|
648
|
if( seenClass ) return 1; |
|
649
|
} |
|
650
|
} |
|
651
|
return seenClass; |
|
652
|
} |
|
653
|
|
|
654
|
/* |
|
655
|
** Look for a file named zName in the check-in with RID=vid. Load the content |
|
656
|
** of that file into pContent and return the RID for the file. Or return 0 |
|
657
|
** if the file is not found or could not be loaded. |
|
658
|
*/ |
|
659
|
int doc_load_content(int vid, const char *zName, Blob *pContent){ |
|
660
|
int writable; |
|
661
|
int rid; /* The RID of the file being loaded */ |
|
662
|
if( db_is_protected(PROTECT_READONLY) |
|
663
|
|| !db_is_writeable("repository") |
|
664
|
){ |
|
665
|
writable = 0; |
|
666
|
}else{ |
|
667
|
writable = 1; |
|
668
|
} |
|
669
|
if( writable ){ |
|
670
|
db_end_transaction(0); |
|
671
|
db_begin_write(); |
|
672
|
} |
|
673
|
if( !db_table_exists("repository", "vcache") || !writable ){ |
|
674
|
db_multi_exec( |
|
675
|
"CREATE %s TABLE IF NOT EXISTS vcache(\n" |
|
676
|
" vid INTEGER, -- check-in ID\n" |
|
677
|
" fname TEXT, -- filename\n" |
|
678
|
" rid INTEGER, -- artifact ID\n" |
|
679
|
" PRIMARY KEY(vid,fname)\n" |
|
680
|
") WITHOUT ROWID", writable ? "" : "TEMPORARY" |
|
681
|
); |
|
682
|
} |
|
683
|
if( !db_exists("SELECT 1 FROM vcache WHERE vid=%d", vid) ){ |
|
684
|
db_multi_exec( |
|
685
|
"DELETE FROM vcache;\n" |
|
686
|
"CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;\n" |
|
687
|
"INSERT INTO vcache(vid,fname,rid)" |
|
688
|
" SELECT checkinID, filename, blob.rid FROM foci, blob" |
|
689
|
" WHERE blob.uuid=foci.uuid" |
|
690
|
" AND foci.checkinID=%d;", |
|
691
|
vid |
|
692
|
); |
|
693
|
} |
|
694
|
rid = db_int(0, "SELECT rid FROM vcache" |
|
695
|
" WHERE vid=%d AND fname=%Q", vid, zName); |
|
696
|
if( rid && content_get(rid, pContent)==0 ){ |
|
697
|
rid = 0; |
|
698
|
} |
|
699
|
return rid; |
|
700
|
} |
|
701
|
|
|
702
|
/* |
|
703
|
** Check to verify that z[i] is contained within HTML markup. |
|
704
|
** |
|
705
|
** This works by looking backwards in the string for the most recent |
|
706
|
** '<' or '>' character. If a '<' is found first, then we assume that |
|
707
|
** z[i] is within markup. If a '>' is seen or neither character is seen, |
|
708
|
** then z[i] is not within markup. |
|
709
|
*/ |
|
710
|
static int isWithinHtmlMarkup(const char *z, int i){ |
|
711
|
while( i>=0 && z[i]!='>' && z[i]!='<' ){ i--; } |
|
712
|
return z[i]=='<'; |
|
713
|
} |
|
714
|
|
|
715
|
/* |
|
716
|
** Check to see if z[i] is contained within an href='...' of markup. |
|
717
|
*/ |
|
718
|
static int isWithinHref(const char *z, int i){ |
|
719
|
while( i>5 |
|
720
|
&& !fossil_isspace(z[i]) |
|
721
|
&& z[i]!='\'' && z[i]!='"' |
|
722
|
&& z[i]!='>' |
|
723
|
){ i--; } |
|
724
|
if( i<=6 ) return 0; |
|
725
|
if( z[i]!='\'' && z[i]!='\"' ) return 0; |
|
726
|
if( strncmp(&z[i-5],"href=",5)!=0 ) return 0; |
|
727
|
if( !fossil_isspace(z[i-6]) ) return 0; |
|
728
|
return 1; |
|
729
|
} |
|
730
|
|
|
731
|
/* |
|
732
|
** Transfer content to the output. During the transfer, when text of |
|
733
|
** the following form is seen: |
|
734
|
** |
|
735
|
** href="$ROOT/..." |
|
736
|
** action="$ROOT/..." |
|
737
|
** href=".../doc/$CURRENT/..." |
|
738
|
** |
|
739
|
** Convert $ROOT to the root URI of the repository, and $CURRENT to the |
|
740
|
** version number of the /doc/ document currently being displayed (if any). |
|
741
|
** Allow ' in place of " and any case for href or action. |
|
742
|
** |
|
743
|
** Efforts are made to limit this translation to cases where the text is |
|
744
|
** fully contained with an HTML markup element. |
|
745
|
*/ |
|
746
|
void convert_href_and_output(Blob *pIn){ |
|
747
|
int i, base; |
|
748
|
int n = blob_size(pIn); |
|
749
|
char *z = blob_buffer(pIn); |
|
750
|
for(base=0, i=7; i<n; i++){ |
|
751
|
if( z[i]=='$' |
|
752
|
&& strncmp(&z[i],"$ROOT/", 6)==0 |
|
753
|
&& (z[i-1]=='\'' || z[i-1]=='"') |
|
754
|
&& i-base>=9 |
|
755
|
&& ((fossil_strnicmp(&z[i-6],"href=",5)==0 && fossil_isspace(z[i-7])) || |
|
756
|
(fossil_strnicmp(&z[i-8],"action=",7)==0 && fossil_isspace(z[i-9])) ) |
|
757
|
&& isWithinHtmlMarkup(z, i-6) |
|
758
|
){ |
|
759
|
blob_append(cgi_output_blob(), &z[base], i-base); |
|
760
|
blob_appendf(cgi_output_blob(), "%R"); |
|
761
|
base = i+5; |
|
762
|
}else |
|
763
|
if( z[i]=='$' |
|
764
|
&& strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0 |
|
765
|
&& isWithinHref(z,i-5) |
|
766
|
&& isWithinHtmlMarkup(z, i-5) |
|
767
|
&& strncmp(g.zPath, "doc/",4)==0 |
|
768
|
){ |
|
769
|
int j; |
|
770
|
for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){} |
|
771
|
blob_append(cgi_output_blob(), &z[base], i-base); |
|
772
|
blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); |
|
773
|
base = i+8; |
|
774
|
} |
|
775
|
} |
|
776
|
blob_append(cgi_output_blob(), &z[base], i-base); |
|
777
|
} |
|
778
|
|
|
779
|
/* |
|
780
|
** Render a document as the reply to the HTTP request. The body |
|
781
|
** of the document is contained in pBody. The body might be binary. |
|
782
|
** The mimetype is in zMimetype. |
|
783
|
*/ |
|
784
|
void document_render( |
|
785
|
Blob *pBody, /* Document content */ |
|
786
|
const char *zMime, /* MIME-type */ |
|
787
|
const char *zDefaultTitle, /* Default title */ |
|
788
|
const char *zFilename /* Name of the file being rendered */ |
|
789
|
){ |
|
790
|
Blob title; |
|
791
|
int isPopup = P("popup")!=0; |
|
792
|
blob_init(&title,0,0); |
|
793
|
if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 ){ |
|
794
|
Blob tail = BLOB_INITIALIZER; |
|
795
|
style_adunit_config(ADUNIT_RIGHT_OK); |
|
796
|
if( wiki_find_title(pBody, &title, &tail) ){ |
|
797
|
if( !isPopup ) style_header("%s", blob_str(&title)); |
|
798
|
wiki_convert(&tail, 0, WIKI_BUTTONS); |
|
799
|
}else{ |
|
800
|
if( !isPopup ) style_header("%s", zDefaultTitle); |
|
801
|
wiki_convert(pBody, 0, WIKI_BUTTONS); |
|
802
|
} |
|
803
|
if( !isPopup ){ |
|
804
|
document_emit_js(); |
|
805
|
style_finish_page(); |
|
806
|
} |
|
807
|
blob_reset(&tail); |
|
808
|
}else if( fossil_strcmp(zMime, "text/x-markdown")==0 ){ |
|
809
|
Blob tail = BLOB_INITIALIZER; |
|
810
|
markdown_to_html(pBody, &title, &tail); |
|
811
|
if( !isPopup ){ |
|
812
|
if( blob_size(&title)>0 ){ |
|
813
|
markdown_dehtmlize_blob(&title); |
|
814
|
style_header("%s", blob_str(&title)); |
|
815
|
}else{ |
|
816
|
style_header("%s", zDefaultTitle); |
|
817
|
} |
|
818
|
} |
|
819
|
convert_href_and_output(&tail); |
|
820
|
if( !isPopup ){ |
|
821
|
document_emit_js(); |
|
822
|
style_finish_page(); |
|
823
|
} |
|
824
|
blob_reset(&tail); |
|
825
|
}else if( fossil_strcmp(zMime, "text/plain")==0 ){ |
|
826
|
style_header("%s", zDefaultTitle); |
|
827
|
@ <blockquote><pre> |
|
828
|
@ %h(blob_str(pBody)) |
|
829
|
@ </pre></blockquote> |
|
830
|
document_emit_js(); |
|
831
|
style_finish_page(); |
|
832
|
}else if( fossil_strcmp(zMime, "text/html")==0 |
|
833
|
&& doc_is_embedded_html(pBody, &title) ){ |
|
834
|
if( blob_size(&title)==0 ) blob_append(&title,zFilename,-1); |
|
835
|
if( !isPopup ) style_header("%s", blob_str(&title)); |
|
836
|
convert_href_and_output(pBody); |
|
837
|
if( !isPopup ){ |
|
838
|
document_emit_js(); |
|
839
|
style_finish_page(); |
|
840
|
} |
|
841
|
}else if( fossil_strcmp(zMime, "text/x-pikchr")==0 ){ |
|
842
|
style_adunit_config(ADUNIT_RIGHT_OK); |
|
843
|
if( !isPopup ) style_header("%s", zDefaultTitle); |
|
844
|
wiki_render_by_mimetype(pBody, zMime); |
|
845
|
if( !isPopup ) style_finish_page(); |
|
846
|
#ifdef FOSSIL_ENABLE_TH1_DOCS |
|
847
|
}else if( Th_AreDocsEnabled() && |
|
848
|
fossil_strcmp(zMime, "application/x-th1")==0 ){ |
|
849
|
int raw = P("raw")!=0; |
|
850
|
if( !raw ){ |
|
851
|
Blob tail; |
|
852
|
blob_zero(&tail); |
|
853
|
if( wiki_find_title(pBody, &title, &tail) ){ |
|
854
|
style_header("%s", blob_str(&title)); |
|
855
|
Th_Render(blob_str(&tail)); |
|
856
|
blob_reset(&tail); |
|
857
|
}else{ |
|
858
|
style_header("%h", zFilename); |
|
859
|
Th_Render(blob_str(pBody)); |
|
860
|
} |
|
861
|
}else{ |
|
862
|
Th_Render(blob_str(pBody)); |
|
863
|
} |
|
864
|
if( !raw ){ |
|
865
|
document_emit_js(); |
|
866
|
style_finish_page(); |
|
867
|
} |
|
868
|
#endif |
|
869
|
}else{ |
|
870
|
fossil_free(style_csp(1)); |
|
871
|
cgi_set_content_type(zMime); |
|
872
|
cgi_set_content(pBody); |
|
873
|
} |
|
874
|
} |
|
875
|
|
|
876
|
|
|
877
|
/* |
|
878
|
** WEBPAGE: uv |
|
879
|
** WEBPAGE: doc |
|
880
|
** URL: /uv/FILE |
|
881
|
** URL: /doc/CHECKIN/FILE |
|
882
|
** |
|
883
|
** CHECKIN can be either tag or hash prefix or timestamp identifying a |
|
884
|
** particular check-in, or the name of a branch (meaning the most recent |
|
885
|
** check-in on that branch) or one of various magic words: |
|
886
|
** |
|
887
|
** "tip" means the most recent check-in |
|
888
|
** |
|
889
|
** "ckout" means the current check-out, if the server is run from |
|
890
|
** within a check-out, otherwise it is the same as "tip" |
|
891
|
** |
|
892
|
** "latest" means use the most recent check-in for the document |
|
893
|
** regardless of what branch it occurs on. |
|
894
|
** |
|
895
|
** FILE is the name of a file to delivered up as a webpage. FILE is relative |
|
896
|
** to the root of the source tree of the repository. The FILE must |
|
897
|
** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read |
|
898
|
** directly from disk and need not be a managed file. For /uv, FILE |
|
899
|
** can also be the hash of the unversioned file. |
|
900
|
** |
|
901
|
** The "ckout" CHECKIN is intended for development - to provide a mechanism |
|
902
|
** for looking at what a file will look like using the /doc webpage after |
|
903
|
** it gets checked in. Some commands like "fossil ui", "fossil server", |
|
904
|
** and "fossil http" accept an argument "--ckout-alias NAME" when allows |
|
905
|
** NAME to be understood as an alias for "ckout". On a site with many |
|
906
|
** embedded hyperlinks to /doc/trunk/... one can run with "--ckout-alias trunk" |
|
907
|
** to simulate what the pending changes will look like after they are |
|
908
|
** checked in. The NAME alias is stored in g.zCkoutAlias. |
|
909
|
** |
|
910
|
** The file extension is used to decide how to render the file. |
|
911
|
** |
|
912
|
** If FILE ends in "/" then the names "FILE/index.html", "FILE/index.wiki", |
|
913
|
** and "FILE/index.md" are tried in that order. If the binary was compiled |
|
914
|
** with TH1 embedded documentation support and the "th1-docs" setting is |
|
915
|
** enabled, the name "FILE/index.th1" is also tried. If none of those are |
|
916
|
** found, then FILE is completely replaced by "404.md" and tried. If that |
|
917
|
** is not found, then a default 404 screen is generated. |
|
918
|
** |
|
919
|
** If the file's mimetype is "text/x-fossil-wiki" or "text/x-markdown" |
|
920
|
** then headers and footers are added. If the document has mimetype |
|
921
|
** text/html then headers and footers are usually not added. However, |
|
922
|
** if a "text/html" document begins with the following div: |
|
923
|
** |
|
924
|
** <div class='fossil-doc' data-title='TEXT'> |
|
925
|
** |
|
926
|
** then headers and footers are supplied. The optional data-title field |
|
927
|
** specifies the title of the document in that case. |
|
928
|
** |
|
929
|
** For fossil-doc documents and for markdown documents, text of the |
|
930
|
** form: "href='$ROOT/" or "action='$ROOT" has the $ROOT name expanded |
|
931
|
** to the top-level of the repository. |
|
932
|
*/ |
|
933
|
void doc_page(void){ |
|
934
|
const char *zName = 0; /* Argument to the /doc page */ |
|
935
|
const char *zOrigName = "?"; /* Original document name */ |
|
936
|
const char *zMime; /* Document MIME type */ |
|
937
|
char *zCheckin = "tip"; /* The check-in holding the document */ |
|
938
|
char *zPathSuffix = ""; /* Text to append to g.zPath */ |
|
939
|
int vid = 0; /* Artifact of check-in */ |
|
940
|
int rid = 0; /* Artifact of file */ |
|
941
|
int i; /* Loop counter */ |
|
942
|
Blob filebody; /* Content of the documentation file */ |
|
943
|
Blob title; /* Document title */ |
|
944
|
int nMiss = (-1); /* Failed attempts to find the document */ |
|
945
|
int isUV = g.zPath[0]=='u'; /* True for /uv. False for /doc */ |
|
946
|
const char *zDfltTitle; |
|
947
|
static const char *const azSuffix[] = { |
|
948
|
"index.html", "index.wiki", "index.md" |
|
949
|
#ifdef FOSSIL_ENABLE_TH1_DOCS |
|
950
|
, "index.th1" |
|
951
|
#endif |
|
952
|
}; |
|
953
|
|
|
954
|
login_check_credentials(); |
|
955
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
956
|
style_set_current_feature("doc"); |
|
957
|
blob_init(&title, 0, 0); |
|
958
|
blob_init(&filebody, 0, 0); |
|
959
|
zDfltTitle = isUV ? "" : "Documentation"; |
|
960
|
db_begin_transaction(); |
|
961
|
while( rid==0 && (++nMiss)<=count(azSuffix) ){ |
|
962
|
zName = P("name"); |
|
963
|
if( isUV ){ |
|
964
|
if( zName==0 ) zName = "index.wiki"; |
|
965
|
i = 0; |
|
966
|
}else{ |
|
967
|
if( zName==0 || zName[0]==0 ) zName = "tip/index.wiki"; |
|
968
|
for(i=0; zName[i] && zName[i]!='/'; i++){} |
|
969
|
zCheckin = mprintf("%.*s", i, zName); |
|
970
|
if( fossil_strcmp(zCheckin,"ckout")==0 && g.localOpen==0 ){ |
|
971
|
zCheckin = "tip"; |
|
972
|
}else if( fossil_strcmp(zCheckin,"latest")==0 ){ |
|
973
|
char *zNewCkin = db_text(0, |
|
974
|
"SELECT uuid FROM blob, mlink, event, filename" |
|
975
|
" WHERE filename.name=%Q" |
|
976
|
" AND mlink.fnid=filename.fnid" |
|
977
|
" AND blob.rid=mlink.mid" |
|
978
|
" AND event.objid=mlink.mid" |
|
979
|
" ORDER BY event.mtime DESC LIMIT 1", |
|
980
|
zName + i + 1); |
|
981
|
if( zNewCkin ) zCheckin = zNewCkin; |
|
982
|
} |
|
983
|
} |
|
984
|
if( nMiss==count(azSuffix) ){ |
|
985
|
zName = "404.md"; |
|
986
|
zDfltTitle = "Not Found"; |
|
987
|
}else if( zName[i]==0 ){ |
|
988
|
assert( nMiss>=0 && nMiss<count(azSuffix) ); |
|
989
|
zName = azSuffix[nMiss]; |
|
990
|
}else if( !isUV ){ |
|
991
|
zName += i; |
|
992
|
} |
|
993
|
while( zName[0]=='/' ){ zName++; } |
|
994
|
if( isUV ){ |
|
995
|
zPathSuffix = fossil_strdup(zName); |
|
996
|
}else{ |
|
997
|
zPathSuffix = mprintf("%s/%s", zCheckin, zName); |
|
998
|
} |
|
999
|
if( nMiss==0 ) zOrigName = zName; |
|
1000
|
if( !file_is_simple_pathname(zName, 1) ){ |
|
1001
|
if( sqlite3_strglob("*/", zName)==0 ){ |
|
1002
|
assert( nMiss>=0 && nMiss<count(azSuffix) ); |
|
1003
|
zName = mprintf("%s%s", zName, azSuffix[nMiss]); |
|
1004
|
if( !file_is_simple_pathname(zName, 1) ){ |
|
1005
|
goto doc_not_found; |
|
1006
|
} |
|
1007
|
}else{ |
|
1008
|
goto doc_not_found; |
|
1009
|
} |
|
1010
|
} |
|
1011
|
if( isUV ){ |
|
1012
|
if( db_table_exists("repository","unversioned") ){ |
|
1013
|
rid = unversioned_content(zName, &filebody); |
|
1014
|
if( rid==1 ){ |
|
1015
|
Stmt q; |
|
1016
|
db_prepare(&q, "SELECT hash, mtime FROM unversioned" |
|
1017
|
" WHERE name=%Q", zName); |
|
1018
|
if( db_step(&q)==SQLITE_ROW ){ |
|
1019
|
etag_check(ETAG_HASH, db_column_text(&q,0)); |
|
1020
|
etag_last_modified(db_column_int64(&q,1)); |
|
1021
|
} |
|
1022
|
db_finalize(&q); |
|
1023
|
}else if( rid==2 ){ |
|
1024
|
zName = db_text(zName, |
|
1025
|
"SELECT name FROM unversioned WHERE hash=%Q", zName); |
|
1026
|
g.isConst = 1; |
|
1027
|
} |
|
1028
|
zDfltTitle = zName; |
|
1029
|
} |
|
1030
|
}else if( fossil_strcmp(zCheckin,"ckout")==0 |
|
1031
|
|| fossil_strcmp(zCheckin,g.zCkoutAlias)==0 |
|
1032
|
){ |
|
1033
|
/* Read from the local check-out */ |
|
1034
|
char *zFullpath; |
|
1035
|
db_must_be_within_tree(); |
|
1036
|
zFullpath = mprintf("%s/%s", g.zLocalRoot, zName); |
|
1037
|
if( file_isfile(zFullpath, RepoFILE) |
|
1038
|
&& blob_read_from_file(&filebody, zFullpath, RepoFILE)>0 ){ |
|
1039
|
rid = 1; /* Fake RID just to get the loop to end */ |
|
1040
|
} |
|
1041
|
fossil_free(zFullpath); |
|
1042
|
}else{ |
|
1043
|
vid = symbolic_name_to_rid(zCheckin, "ci"); |
|
1044
|
rid = vid>0 ? doc_load_content(vid, zName, &filebody) : 0; |
|
1045
|
} |
|
1046
|
} |
|
1047
|
g.zPath = mprintf("%s/%s", g.zPath, zPathSuffix); |
|
1048
|
if( rid==0 ) goto doc_not_found; |
|
1049
|
blob_to_utf8_no_bom(&filebody, 0); |
|
1050
|
|
|
1051
|
/* The file is now contained in the filebody blob. Deliver the |
|
1052
|
** file to the user |
|
1053
|
*/ |
|
1054
|
zMime = nMiss==0 ? P("mimetype") : 0; |
|
1055
|
if( zMime==0 ){ |
|
1056
|
zMime = mimetype_from_name(zName); |
|
1057
|
} |
|
1058
|
Th_StoreUnsafe("doc_name", zName); |
|
1059
|
if( vid ){ |
|
1060
|
Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" |
|
1061
|
" FROM blob WHERE rid=%d", vid)); |
|
1062
|
Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" |
|
1063
|
" WHERE objid=%d AND type='ci'", vid)); |
|
1064
|
} |
|
1065
|
cgi_check_for_malice(); |
|
1066
|
document_render(&filebody, zMime, zDfltTitle, zName); |
|
1067
|
if( nMiss>=count(azSuffix) ) cgi_set_status(404, "Not Found"); |
|
1068
|
db_end_transaction(0); |
|
1069
|
blob_reset(&title); |
|
1070
|
blob_reset(&filebody); |
|
1071
|
return; |
|
1072
|
|
|
1073
|
/* Jump here when unable to locate the document */ |
|
1074
|
doc_not_found: |
|
1075
|
db_end_transaction(0); |
|
1076
|
if( isUV && P("name")==0 ){ |
|
1077
|
uvlist_page(); |
|
1078
|
return; |
|
1079
|
} |
|
1080
|
cgi_set_status(404, "Not Found"); |
|
1081
|
style_header("Not Found"); |
|
1082
|
@ <p>Document %h(zOrigName) not found |
|
1083
|
if( fossil_strcmp(zCheckin,"ckout")!=0 ){ |
|
1084
|
@ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin)</a> |
|
1085
|
} |
|
1086
|
style_finish_page(); |
|
1087
|
blob_reset(&title); |
|
1088
|
blob_reset(&filebody); |
|
1089
|
return; |
|
1090
|
} |
|
1091
|
|
|
1092
|
/* |
|
1093
|
** The default logo. |
|
1094
|
*/ |
|
1095
|
static const unsigned char aLogo[] = { |
|
1096
|
71, 73, 70, 56, 55, 97, 62, 0, 71, 0, 244, 0, 0, 85, |
|
1097
|
129, 149, 95, 136, 155, 99, 139, 157, 106, 144, 162, 113, 150, 166, |
|
1098
|
116, 152, 168, 127, 160, 175, 138, 168, 182, 148, 176, 188, 159, 184, |
|
1099
|
195, 170, 192, 202, 180, 199, 208, 184, 202, 210, 191, 207, 215, 201, |
|
1100
|
215, 221, 212, 223, 228, 223, 231, 235, 226, 227, 226, 226, 234, 237, |
|
1101
|
233, 239, 241, 240, 244, 246, 244, 247, 248, 255, 255, 255, 0, 0, |
|
1102
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
1103
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, |
|
1104
|
0, 0, 62, 0, 71, 0, 0, 5, 255, 96, 100, 141, 100, 105, |
|
1105
|
158, 168, 37, 41, 132, 192, 164, 112, 44, 207, 102, 99, 0, 56, |
|
1106
|
16, 84, 116, 239, 199, 141, 65, 110, 232, 248, 25, 141, 193, 161, |
|
1107
|
82, 113, 108, 202, 32, 55, 229, 210, 73, 61, 41, 164, 88, 102, |
|
1108
|
181, 10, 41, 96, 179, 91, 106, 35, 240, 5, 135, 143, 137, 242, |
|
1109
|
87, 123, 246, 33, 190, 81, 108, 163, 237, 198, 14, 30, 113, 233, |
|
1110
|
131, 78, 115, 72, 11, 115, 87, 101, 19, 124, 51, 66, 74, 8, |
|
1111
|
19, 16, 67, 100, 74, 133, 50, 15, 101, 135, 56, 11, 74, 6, |
|
1112
|
143, 49, 126, 106, 56, 8, 145, 67, 9, 152, 48, 139, 155, 5, |
|
1113
|
22, 13, 74, 115, 161, 41, 147, 101, 13, 130, 57, 132, 170, 40, |
|
1114
|
167, 155, 0, 94, 57, 3, 178, 48, 183, 181, 57, 160, 186, 40, |
|
1115
|
19, 141, 189, 0, 69, 192, 40, 16, 195, 155, 185, 199, 41, 201, |
|
1116
|
189, 191, 205, 193, 188, 131, 210, 49, 175, 88, 209, 214, 38, 19, |
|
1117
|
3, 11, 19, 111, 127, 60, 219, 39, 55, 204, 19, 11, 6, 100, |
|
1118
|
5, 10, 227, 228, 37, 163, 0, 239, 117, 56, 238, 243, 49, 195, |
|
1119
|
177, 247, 48, 158, 56, 251, 50, 216, 254, 197, 56, 128, 107, 158, |
|
1120
|
2, 125, 171, 114, 92, 218, 246, 96, 66, 3, 4, 50, 134, 176, |
|
1121
|
145, 6, 97, 64, 144, 24, 19, 136, 108, 91, 177, 160, 0, 194, |
|
1122
|
19, 253, 0, 216, 107, 214, 224, 192, 129, 5, 16, 83, 255, 244, |
|
1123
|
43, 213, 195, 24, 159, 27, 169, 64, 230, 88, 208, 227, 129, 182, |
|
1124
|
54, 4, 89, 158, 24, 181, 163, 199, 1, 155, 52, 233, 8, 130, |
|
1125
|
176, 83, 24, 128, 137, 50, 18, 32, 48, 48, 114, 11, 173, 137, |
|
1126
|
19, 110, 4, 64, 105, 1, 194, 30, 140, 68, 15, 24, 24, 224, |
|
1127
|
50, 76, 70, 0, 11, 171, 54, 26, 160, 181, 194, 149, 148, 40, |
|
1128
|
174, 148, 122, 64, 180, 208, 161, 17, 207, 112, 164, 1, 128, 96, |
|
1129
|
148, 78, 18, 21, 194, 33, 229, 51, 247, 65, 133, 97, 5, 250, |
|
1130
|
69, 229, 100, 34, 220, 128, 166, 116, 190, 62, 8, 167, 195, 170, |
|
1131
|
47, 163, 0, 130, 90, 152, 11, 160, 173, 170, 27, 154, 26, 91, |
|
1132
|
232, 151, 171, 18, 14, 162, 253, 98, 170, 18, 70, 171, 64, 219, |
|
1133
|
10, 67, 136, 134, 187, 116, 75, 180, 46, 179, 174, 135, 4, 189, |
|
1134
|
229, 231, 78, 40, 10, 62, 226, 164, 172, 64, 240, 167, 170, 10, |
|
1135
|
18, 124, 188, 10, 107, 65, 193, 94, 11, 93, 171, 28, 248, 17, |
|
1136
|
239, 46, 140, 78, 97, 34, 25, 153, 36, 99, 65, 130, 7, 203, |
|
1137
|
183, 168, 51, 34, 136, 25, 140, 10, 6, 16, 28, 255, 145, 241, |
|
1138
|
230, 140, 10, 66, 178, 167, 112, 48, 192, 128, 129, 9, 31, 141, |
|
1139
|
84, 138, 63, 163, 162, 2, 203, 206, 240, 56, 55, 98, 192, 188, |
|
1140
|
15, 185, 50, 160, 6, 0, 125, 62, 33, 214, 195, 33, 5, 24, |
|
1141
|
184, 25, 231, 14, 201, 245, 144, 23, 126, 104, 228, 0, 145, 2, |
|
1142
|
13, 140, 244, 212, 17, 21, 20, 176, 159, 17, 95, 225, 160, 128, |
|
1143
|
16, 1, 32, 224, 142, 32, 227, 125, 87, 64, 0, 16, 54, 129, |
|
1144
|
205, 2, 141, 76, 53, 130, 103, 37, 166, 64, 144, 107, 78, 196, |
|
1145
|
5, 192, 0, 54, 50, 229, 9, 141, 49, 84, 194, 35, 12, 196, |
|
1146
|
153, 48, 192, 137, 57, 84, 24, 7, 87, 159, 249, 240, 215, 143, |
|
1147
|
105, 241, 118, 149, 9, 139, 4, 64, 203, 141, 35, 140, 129, 131, |
|
1148
|
16, 222, 125, 231, 128, 2, 238, 17, 152, 66, 3, 5, 56, 224, |
|
1149
|
159, 103, 16, 76, 25, 75, 5, 11, 164, 215, 96, 9, 14, 16, |
|
1150
|
36, 225, 15, 11, 40, 144, 192, 156, 41, 10, 178, 199, 3, 66, |
|
1151
|
64, 80, 193, 3, 124, 90, 48, 129, 129, 102, 177, 18, 192, 154, |
|
1152
|
49, 84, 240, 208, 92, 22, 149, 96, 39, 9, 31, 74, 17, 94, |
|
1153
|
3, 8, 177, 199, 72, 59, 85, 76, 25, 216, 8, 139, 194, 197, |
|
1154
|
138, 163, 69, 96, 115, 0, 147, 72, 72, 84, 28, 14, 79, 86, |
|
1155
|
233, 230, 23, 113, 26, 160, 128, 3, 10, 58, 129, 103, 14, 159, |
|
1156
|
214, 163, 146, 117, 238, 213, 154, 128, 151, 109, 84, 64, 217, 13, |
|
1157
|
27, 10, 228, 39, 2, 235, 164, 168, 74, 8, 0, 59, |
|
1158
|
}; |
|
1159
|
|
|
1160
|
/* |
|
1161
|
** WEBPAGE: logo |
|
1162
|
** |
|
1163
|
** Return the logo image. This image is available to anybody who can see |
|
1164
|
** the login page. It is designed for use in the upper left-hand corner |
|
1165
|
** of the header. |
|
1166
|
*/ |
|
1167
|
void logo_page(void){ |
|
1168
|
Blob logo; |
|
1169
|
char *zMime; |
|
1170
|
|
|
1171
|
etag_check(ETAG_CONFIG, 0); |
|
1172
|
zMime = db_get("logo-mimetype", "image/gif"); |
|
1173
|
blob_zero(&logo); |
|
1174
|
db_blob(&logo, "SELECT value FROM config WHERE name='logo-image'"); |
|
1175
|
if( blob_size(&logo)==0 ){ |
|
1176
|
blob_init(&logo, (char*)aLogo, sizeof(aLogo)); |
|
1177
|
} |
|
1178
|
cgi_set_content_type(zMime); |
|
1179
|
cgi_set_content(&logo); |
|
1180
|
} |
|
1181
|
|
|
1182
|
/* |
|
1183
|
** The default background image: a 16x16 white GIF |
|
1184
|
*/ |
|
1185
|
static const unsigned char aBackground[] = { |
|
1186
|
71, 73, 70, 56, 57, 97, 16, 0, 16, 0, |
|
1187
|
240, 0, 0, 255, 255, 255, 0, 0, 0, 33, |
|
1188
|
254, 4, 119, 105, 115, 104, 0, 44, 0, 0, |
|
1189
|
0, 0, 16, 0, 16, 0, 0, 2, 14, 132, |
|
1190
|
143, 169, 203, 237, 15, 163, 156, 180, 218, 139, |
|
1191
|
179, 62, 5, 0, 59, |
|
1192
|
}; |
|
1193
|
|
|
1194
|
|
|
1195
|
/* |
|
1196
|
** WEBPAGE: background |
|
1197
|
** |
|
1198
|
** Return the background image. If no background image is defined, a |
|
1199
|
** built-in 16x16 pixel white GIF is returned. |
|
1200
|
*/ |
|
1201
|
void background_page(void){ |
|
1202
|
Blob bgimg; |
|
1203
|
char *zMime; |
|
1204
|
|
|
1205
|
etag_check(ETAG_CONFIG, 0); |
|
1206
|
zMime = db_get("background-mimetype", "image/gif"); |
|
1207
|
blob_zero(&bgimg); |
|
1208
|
db_blob(&bgimg, "SELECT value FROM config WHERE name='background-image'"); |
|
1209
|
if( blob_size(&bgimg)==0 ){ |
|
1210
|
blob_init(&bgimg, (char*)aBackground, sizeof(aBackground)); |
|
1211
|
} |
|
1212
|
cgi_set_content_type(zMime); |
|
1213
|
cgi_set_content(&bgimg); |
|
1214
|
} |
|
1215
|
|
|
1216
|
|
|
1217
|
/* |
|
1218
|
** WEBPAGE: favicon.ico |
|
1219
|
** |
|
1220
|
** Return the configured "favicon.ico" image. If no "favicon.ico" image |
|
1221
|
** is defined, the returned image is for the Fossil lizard icon. |
|
1222
|
** |
|
1223
|
** The intended use case here is to supply an icon for the "fossil ui" |
|
1224
|
** command. For a permanent website, the recommended process is for |
|
1225
|
** the admin to set up a project-specific icon and reference that icon |
|
1226
|
** in the HTML header using a line like: |
|
1227
|
** |
|
1228
|
** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> |
|
1229
|
** |
|
1230
|
*/ |
|
1231
|
void favicon_page(void){ |
|
1232
|
Blob icon; |
|
1233
|
char *zMime; |
|
1234
|
|
|
1235
|
etag_check(ETAG_CONFIG, 0); |
|
1236
|
zMime = db_get("icon-mimetype", "image/gif"); |
|
1237
|
blob_zero(&icon); |
|
1238
|
db_blob(&icon, "SELECT value FROM config WHERE name='icon-image'"); |
|
1239
|
if( blob_size(&icon)==0 ){ |
|
1240
|
blob_init(&icon, (char*)aLogo, sizeof(aLogo)); |
|
1241
|
} |
|
1242
|
cgi_set_content_type(zMime); |
|
1243
|
cgi_set_content(&icon); |
|
1244
|
} |
|
1245
|
|
|
1246
|
/* |
|
1247
|
** WEBPAGE: docsrch |
|
1248
|
** |
|
1249
|
** Search for documents that match a user-supplied full-text search pattern. |
|
1250
|
** If no pattern is specified (by the s= query parameter) then the user |
|
1251
|
** is prompted to enter a search string. |
|
1252
|
** |
|
1253
|
** Query parameters: |
|
1254
|
** |
|
1255
|
** s=PATTERN Search for PATTERN |
|
1256
|
*/ |
|
1257
|
void doc_search_page(void){ |
|
1258
|
const int isSearch = P("s")!=0; |
|
1259
|
login_check_credentials(); |
|
1260
|
style_header("Document Search%s", isSearch ? " Results" : ""); |
|
1261
|
cgi_check_for_malice(); |
|
1262
|
search_screen(SRCH_DOC, 0); |
|
1263
|
style_finish_page(); |
|
1264
|
} |
|
1265
|
|