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