]> granicus.if.org Git - apache/blob - modules/mappers/mod_speling.c
*) continued header revamping
[apache] / modules / mappers / mod_speling.c
1 /* ====================================================================
2  * The Apache Software License, Version 1.1
3  *
4  * Copyright (c) 2000 The Apache Software Foundation.  All rights
5  * reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  *
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in
16  *    the documentation and/or other materials provided with the
17  *    distribution.
18  *
19  * 3. The end-user documentation included with the redistribution,
20  *    if any, must include the following acknowledgment:
21  *       "This product includes software developed by the
22  *        Apache Software Foundation (http://www.apache.org/)."
23  *    Alternately, this acknowledgment may appear in the software itself,
24  *    if and wherever such third-party acknowledgments normally appear.
25  *
26  * 4. The names "Apache" and "Apache Software Foundation" must
27  *    not be used to endorse or promote products derived from this
28  *    software without prior written permission. For written
29  *    permission, please contact apache@apache.org.
30  *
31  * 5. Products derived from this software may not be called "Apache",
32  *    nor may "Apache" appear in their name, without prior written
33  *    permission of the Apache Software Foundation.
34  *
35  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38  * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
46  * SUCH DAMAGE.
47  * ====================================================================
48  *
49  * This software consists of voluntary contributions made by many
50  * individuals on behalf of the Apache Software Foundation.  For more
51  * information on the Apache Software Foundation, please see
52  * <http://www.apache.org/>.
53  *
54  * Portions of this software are based upon public domain software
55  * originally written at the National Center for Supercomputing Applications,
56  * University of Illinois, Urbana-Champaign.
57  */
58
59 #include "apr.h"
60 #include "apr_file_io.h"
61 #include "apr_strings.h"
62
63 #define APR_WANT_STRFUNC
64 #include "apr_want.h"
65
66 #define WANT_BASENAME_MATCH
67
68 #include "httpd.h"
69 #include "http_core.h"
70 #include "http_config.h"
71 #include "http_request.h"
72 #include "http_log.h"
73
74 /* mod_speling.c - by Alexei Kosut <akosut@organic.com> June, 1996
75  *
76  * This module is transparent, and simple. It attempts to correct
77  * misspellings of URLs that users might have entered, namely by checking
78  * capitalizations. If it finds a match, it sends a redirect.
79  *
80  * 08-Aug-1997 <Martin.Kraemer@Mch.SNI.De>
81  * o Upgraded module interface to apache_1.3a2-dev API (more NULL's in
82  *   speling_module).
83  * o Integrated tcsh's "spelling correction" routine which allows one
84  *   misspelling (character insertion/omission/typo/transposition).
85  *   Rewrote it to ignore case as well. This ought to catch the majority
86  *   of misspelled requests.
87  * o Commented out the second pass where files' suffixes are stripped.
88  *   Given the better hit rate of the first pass, this rather ugly
89  *   (request index.html, receive index.db ?!?!) solution can be
90  *   omitted.
91  * o wrote a "kind of" html page for mod_speling
92  *
93  * Activate it with "CheckSpelling On"
94  */
95
96 AP_MODULE_DECLARE_DATA module speling_module;
97
98 typedef struct {
99     int enabled;
100 } spconfig;
101
102 /*
103  * Create a configuration specific to this module for a server or directory
104  * location, and fill it with the default settings.
105  *
106  * The API says that in the absence of a merge function, the record for the
107  * closest ancestor is used exclusively.  That's what we want, so we don't
108  * bother to have such a function.
109  */
110
111 static void *mkconfig(apr_pool_t *p)
112 {
113     spconfig *cfg = apr_pcalloc(p, sizeof(spconfig));
114
115     cfg->enabled = 0;
116     return cfg;
117 }
118
119 /*
120  * Respond to a callback to create configuration record for a server or
121  * vhost environment.
122  */
123 static void *create_mconfig_for_server(apr_pool_t *p, server_rec *s)
124 {
125     return mkconfig(p);
126 }
127
128 /*
129  * Respond to a callback to create a config record for a specific directory.
130  */
131 static void *create_mconfig_for_directory(apr_pool_t *p, char *dir)
132 {
133     return mkconfig(p);
134 }
135
136 /*
137  * Handler for the CheckSpelling directive, which is FLAG.
138  */
139 static const char *set_speling(cmd_parms *cmd, void *mconfig, int arg)
140 {
141     spconfig *cfg = (spconfig *) mconfig;
142
143     cfg->enabled = arg;
144     return NULL;
145 }
146
147 /*
148  * Define the directives specific to this module.  This structure is referenced
149  * later by the 'module' structure.
150  */
151 static const command_rec speling_cmds[] =
152 {
153     AP_INIT_FLAG("CheckSpelling", set_speling, NULL, OR_OPTIONS,
154                  "whether or not to fix miscapitalized/misspelled requests"),
155     { NULL }
156 };
157
158 typedef enum {
159     SP_IDENTICAL = 0,
160     SP_MISCAPITALIZED = 1,
161     SP_TRANSPOSITION = 2,
162     SP_MISSINGCHAR = 3,
163     SP_EXTRACHAR = 4,
164     SP_SIMPLETYPO = 5,
165     SP_VERYDIFFERENT = 6
166 } sp_reason;
167
168 static const char *sp_reason_str[] =
169 {
170     "identical",
171     "miscapitalized",
172     "transposed characters",
173     "character missing",
174     "extra character",
175     "mistyped character",
176     "common basename",
177 };
178
179 typedef struct {
180     const char *name;
181     sp_reason quality;
182 } misspelled_file;
183
184 /*
185  * spdist() is taken from Kernighan & Pike,
186  *  _The_UNIX_Programming_Environment_
187  * and adapted somewhat to correspond better to psychological reality.
188  * (Note the changes to the return values)
189  *
190  * According to Pollock and Zamora, CACM April 1984 (V. 27, No. 4),
191  * page 363, the correct order for this is:
192  * OMISSION = TRANSPOSITION > INSERTION > SUBSTITUTION
193  * thus, it was exactly backwards in the old version. -- PWP
194  *
195  * This routine was taken out of tcsh's spelling correction code
196  * (tcsh-6.07.04) and re-converted to apache data types ("char" type
197  * instead of tcsh's NLS'ed "Char"). Plus it now ignores the case
198  * during comparisons, so is a "approximate strcasecmp()".
199  * NOTE that is still allows only _one_ real "typo",
200  * it does NOT try to correct multiple errors.
201  */
202
203 static sp_reason spdist(const char *s, const char *t)
204 {
205     for (; apr_tolower(*s) == apr_tolower(*t); t++, s++) {
206         if (*t == '\0') {
207             return SP_MISCAPITALIZED;   /* exact match (sans case) */
208         }
209     }
210     if (*s) {
211         if (*t) {
212             if (s[1] && t[1] && apr_tolower(*s) == apr_tolower(t[1])
213                 && apr_tolower(*t) == apr_tolower(s[1])
214                 && strcasecmp(s + 2, t + 2) == 0) {
215                 return SP_TRANSPOSITION;        /* transposition */
216             }
217             if (strcasecmp(s + 1, t + 1) == 0) {
218                 return SP_SIMPLETYPO;   /* 1 char mismatch */
219             }
220         }
221         if (strcasecmp(s + 1, t) == 0) {
222             return SP_EXTRACHAR;        /* extra character */
223         }
224     }
225     if (*t && strcasecmp(s, t + 1) == 0) {
226         return SP_MISSINGCHAR;  /* missing character */
227     }
228     return SP_VERYDIFFERENT;    /* distance too large to fix. */
229 }
230
231 static int sort_by_quality(const void *left, const void *rite)
232 {
233     return (int) (((misspelled_file *) left)->quality)
234         - (int) (((misspelled_file *) rite)->quality);
235 }
236
237 static int check_speling(request_rec *r)
238 {
239     spconfig *cfg;
240     char *good, *bad, *postgood, *url;
241     apr_finfo_t dirent;
242     int filoc, dotloc, urlen, pglen;
243     apr_array_header_t *candidates = NULL;
244     apr_dir_t          *dir;
245
246     cfg = ap_get_module_config(r->per_dir_config, &speling_module);
247     if (!cfg->enabled) {
248         return DECLINED;
249     }
250
251     /* We only want to worry about GETs */
252     if (r->method_number != M_GET) {
253         return DECLINED;
254     }
255
256     /* We've already got a file of some kind or another */
257     if (r->proxyreq || (r->finfo.filetype != 0)) {
258         return DECLINED;
259     }
260
261     /* This is a sub request - don't mess with it */
262     if (r->main) {
263         return DECLINED;
264     }
265
266     /*
267      * The request should end up looking like this:
268      * r->uri: /correct-url/mispelling/more
269      * r->filename: /correct-file/mispelling r->path_info: /more
270      *
271      * So we do this in steps. First break r->filename into two pieces
272      */
273
274     filoc = ap_rind(r->filename, '/');
275     /*
276      * Don't do anything if the request doesn't contain a slash, or
277      * requests "/" 
278      */
279     if (filoc == -1 || strcmp(r->uri, "/") == 0) {
280         return DECLINED;
281     }
282
283     /* good = /correct-file */
284     good = apr_pstrndup(r->pool, r->filename, filoc);
285     /* bad = mispelling */
286     bad = apr_pstrdup(r->pool, r->filename + filoc + 1);
287     /* postgood = mispelling/more */
288     postgood = apr_pstrcat(r->pool, bad, r->path_info, NULL);
289
290     urlen = strlen(r->uri);
291     pglen = strlen(postgood);
292
293     /* Check to see if the URL pieces add up */
294     if (strcmp(postgood, r->uri + (urlen - pglen))) {
295         return DECLINED;
296     }
297
298     /* url = /correct-url */
299     url = apr_pstrndup(r->pool, r->uri, (urlen - pglen));
300
301     /* Now open the directory and do ourselves a check... */
302     if (apr_dir_open(&dir, good, r->pool) != APR_SUCCESS) {
303         /* Oops, not a directory... */
304         return DECLINED;
305     }
306
307     candidates = apr_array_make(r->pool, 2, sizeof(misspelled_file));
308
309     dotloc = ap_ind(bad, '.');
310     if (dotloc == -1) {
311         dotloc = strlen(bad);
312     }
313
314     while (apr_dir_read(&dirent, APR_FINFO_DIRENT, dir) == APR_SUCCESS) {
315         sp_reason q;
316
317         /*
318          * If we end up with a "fixed" URL which is identical to the
319          * requested one, we must have found a broken symlink or some such.
320          * Do _not_ try to redirect this, it causes a loop!
321          */
322         if (strcmp(bad, dirent.name) == 0) {
323             apr_dir_close(dir);
324             return OK;
325         }
326
327         /*
328          * miscapitalization errors are checked first (like, e.g., lower case
329          * file, upper case request)
330          */
331         else if (strcasecmp(bad, dirent.name) == 0) {
332             misspelled_file *sp_new;
333
334             sp_new = (misspelled_file *) apr_array_push(candidates);
335             sp_new->name = apr_pstrdup(r->pool, dirent.name);
336             sp_new->quality = SP_MISCAPITALIZED;
337         }
338
339         /*
340          * simple typing errors are checked next (like, e.g.,
341          * missing/extra/transposed char)
342          */
343         else if ((q = spdist(bad, dirent.name)) != SP_VERYDIFFERENT) {
344             misspelled_file *sp_new;
345
346             sp_new = (misspelled_file *) apr_array_push(candidates);
347             sp_new->name = apr_pstrdup(r->pool, dirent.name);
348             sp_new->quality = q;
349         }
350
351         /*
352          * The spdist() should have found the majority of the misspelled
353          * requests.  It is of questionable use to continue looking for
354          * files with the same base name, but potentially of totally wrong
355          * type (index.html <-> index.db).
356          * I would propose to not set the WANT_BASENAME_MATCH define.
357          *      08-Aug-1997 <Martin.Kraemer@Mch.SNI.De>
358          *
359          * However, Alexei replied giving some reasons to add it anyway:
360          * > Oh, by the way, I remembered why having the
361          * > extension-stripping-and-matching stuff is a good idea:
362          * >
363          * > If you're using MultiViews, and have a file named foobar.html,
364          * > which you refer to as "foobar", and someone tried to access
365          * > "Foobar", mod_speling won't find it, because it won't find
366          * > anything matching that spelling. With the extension-munging,
367          * > it would locate "foobar.html". Not perfect, but I ran into
368          * > that problem when I first wrote the module.
369          */
370         else {
371 #ifdef WANT_BASENAME_MATCH
372             /*
373              * Okay... we didn't find anything. Now we take out the hard-core
374              * power tools. There are several cases here. Someone might have
375              * entered a wrong extension (.htm instead of .html or vice
376              * versa) or the document could be negotiated. At any rate, now
377              * we just compare stuff before the first dot. If it matches, we
378              * figure we got us a match. This can result in wrong things if
379              * there are files of different content types but the same prefix
380              * (e.g. foo.gif and foo.html) This code will pick the first one
381              * it finds. Better than a Not Found, though.
382              */
383             int entloc = ap_ind(dirent.name, '.');
384             if (entloc == -1) {
385                 entloc = strlen(dirent.name);
386             }
387
388             if ((dotloc == entloc)
389                 && !strncasecmp(bad, dirent.name, dotloc)) {
390                 misspelled_file *sp_new;
391
392                 sp_new = (misspelled_file *) apr_array_push(candidates);
393                 sp_new->name = apr_pstrdup(r->pool, dirent.name);
394                 sp_new->quality = SP_VERYDIFFERENT;
395             }
396 #endif
397         }
398     }
399     apr_dir_close(dir);
400
401     if (candidates->nelts != 0) {
402         /* Wow... we found us a mispelling. Construct a fixed url */
403         char *nuri;
404         const char *ref;
405         misspelled_file *variant = (misspelled_file *) candidates->elts;
406         int i;
407
408         ref = apr_table_get(r->headers_in, "Referer");
409
410         qsort((void *) candidates->elts, candidates->nelts,
411               sizeof(misspelled_file), sort_by_quality);
412
413         /*
414          * Conditions for immediate redirection: 
415          *     a) the first candidate was not found by stripping the suffix 
416          * AND b) there exists only one candidate OR the best match is not
417          *        ambiguous
418          * then return a redirection right away.
419          */
420         if (variant[0].quality != SP_VERYDIFFERENT
421             && (candidates->nelts == 1
422                 || variant[0].quality != variant[1].quality)) {
423
424             nuri = ap_escape_uri(r->pool, apr_pstrcat(r->pool, url,
425                                                      variant[0].name,
426                                                      r->path_info, NULL));
427             if (r->parsed_uri.query)
428                 nuri = apr_pstrcat(r->pool, nuri, "?", r->parsed_uri.query, NULL);
429
430             apr_table_setn(r->headers_out, "Location",
431                           ap_construct_url(r->pool, nuri, r));
432
433             ap_log_rerror(APLOG_MARK, APLOG_NOERRNO | APLOG_INFO, APR_SUCCESS,
434                           r, 
435                           ref ? "Fixed spelling: %s to %s from %s"
436                               : "Fixed spelling: %s to %s",
437                           r->uri, nuri, ref);
438
439             return HTTP_MOVED_PERMANENTLY;
440         }
441         /*
442          * Otherwise, a "[300] Multiple Choices" list with the variants is
443          * returned.
444          */
445         else {
446             apr_pool_t *p;
447             apr_table_t *notes;
448             apr_pool_t *sub_pool;
449             apr_array_header_t *t;
450             apr_array_header_t *v;
451
452
453             if (r->main == NULL) {
454                 p = r->pool;
455                 notes = r->notes;
456             }
457             else {
458                 p = r->main->pool;
459                 notes = r->main->notes;
460             }
461
462             if (apr_pool_create(&sub_pool, p) != APR_SUCCESS)
463                 return DECLINED;
464
465             t = apr_array_make(sub_pool, candidates->nelts * 8 + 8,
466                               sizeof(char *));
467             v = apr_array_make(sub_pool, candidates->nelts * 5,
468                               sizeof(char *));
469
470             /* Generate the response text. */
471
472             *(const char **)apr_array_push(t) =
473                           "The document name you requested (<code>";
474             *(const char **)apr_array_push(t) = ap_escape_html(sub_pool, r->uri);
475             *(const char **)apr_array_push(t) =
476                            "</code>) could not be found on this server.\n"
477                            "However, we found documents with names similar "
478                            "to the one you requested.<p>"
479                            "Available documents:\n<ul>\n";
480
481             for (i = 0; i < candidates->nelts; ++i) {
482                 char *vuri;
483                 const char *reason;
484
485                 reason = sp_reason_str[(int) (variant[i].quality)];
486                 /* The format isn't very neat... */
487                 vuri = apr_pstrcat(sub_pool, url, variant[i].name, r->path_info,
488                                   (r->parsed_uri.query != NULL) ? "?" : "",
489                                   (r->parsed_uri.query != NULL)
490                                       ? r->parsed_uri.query : "",
491                                   NULL);
492                 *(const char **)apr_array_push(v) = "\"";
493                 *(const char **)apr_array_push(v) = ap_escape_uri(sub_pool, vuri);
494                 *(const char **)apr_array_push(v) = "\";\"";
495                 *(const char **)apr_array_push(v) = reason;
496                 *(const char **)apr_array_push(v) = "\"";
497
498                 *(const char **)apr_array_push(t) = "<li><a href=\"";
499                 *(const char **)apr_array_push(t) = ap_escape_uri(sub_pool, vuri);
500                 *(const char **)apr_array_push(t) = "\">";
501                 *(const char **)apr_array_push(t) = ap_escape_html(sub_pool, vuri);
502                 *(const char **)apr_array_push(t) = "</a> (";
503                 *(const char **)apr_array_push(t) = reason;
504                 *(const char **)apr_array_push(t) = ")\n";
505
506                 /*
507                  * when we have printed the "close matches" and there are
508                  * more "distant matches" (matched by stripping the suffix),
509                  * then we insert an additional separator text to suggest
510                  * that the user LOOK CLOSELY whether these are really the
511                  * files she wanted.
512                  */
513                 if (i > 0 && i < candidates->nelts - 1
514                     && variant[i].quality != SP_VERYDIFFERENT
515                     && variant[i + 1].quality == SP_VERYDIFFERENT) {
516                     *(const char **)apr_array_push(t) = 
517                                    "</ul>\nFurthermore, the following related "
518                                    "documents were found:\n<ul>\n";
519                 }
520             }
521             *(const char **)apr_array_push(t) = "</ul>\n";
522
523             /* If we know there was a referring page, add a note: */
524             if (ref != NULL) {
525                 *(const char **)apr_array_push(t) =
526                                "Please consider informing the owner of the "
527                                "<a href=\"";
528                 *(const char **)apr_array_push(t) = ap_escape_uri(sub_pool, ref);
529                 *(const char **)apr_array_push(t) = "\">referring page</a> "
530                                "about the broken link.\n";
531             }
532
533
534             /* Pass our apr_table_t to http_protocol.c (see mod_negotiation): */
535             apr_table_setn(notes, "variant-list", apr_array_pstrcat(p, t, 0));
536
537             apr_table_mergen(r->subprocess_env, "VARIANTS",
538                             apr_array_pstrcat(p, v, ','));
539           
540             apr_pool_destroy(sub_pool);
541
542             ap_log_rerror(APLOG_MARK, APLOG_NOERRNO | APLOG_INFO, 0, r,
543                          ref ? "Spelling fix: %s: %d candidates from %s"
544                              : "Spelling fix: %s: %d candidates",
545                          r->uri, candidates->nelts, ref);
546
547             return HTTP_MULTIPLE_CHOICES;
548         }
549     }
550
551     return OK;
552 }
553
554 static void register_hooks(apr_pool_t *p)
555 {
556     ap_hook_fixups(check_speling,NULL,NULL,APR_HOOK_LAST);
557 }
558
559 module AP_MODULE_DECLARE_DATA speling_module =
560 {
561     STANDARD20_MODULE_STUFF,
562     create_mconfig_for_directory,  /* create per-dir config */
563     NULL,                       /* merge per-dir config */
564     create_mconfig_for_server,  /* server config */
565     NULL,                       /* merge server config */
566     speling_cmds,               /* command apr_table_t */
567     register_hooks              /* register hooks */
568 };