1 /* Copyright 1999-2005 The Apache Software Foundation or its licensors, as
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * This imagemap module started as a port of the original imagemap.c
19 * written by Rob McCool (11/13/93 robm@ncsa.uiuc.edu).
20 * This version includes the mapping algorithms found in version 1.3
23 * Contributors to this code include:
25 * Kevin Hughes, kevinh@pulua.hcc.hawaii.edu
27 * Eric Haines, erich@eye.com
28 * "macmartinized" polygon code copyright 1992 by Eric Haines, erich@eye.com
30 * Randy Terbush, randy@zyzzyva.com
31 * port to Apache module format, "base_uri" and support for relative URLs
33 * James H. Cloos, Jr., cloos@jhcloos.com
34 * Added point datatype, using code in NCSA's version 1.8 imagemap.c
35 * program, as distributed with version 1.4.1 of their server.
36 * The point code is originally added by Craig Milo Rogers, Rogers@ISI.Edu
38 * Nathan Kurz, nate@tripod.com
39 * Rewrite/reorganization. New handling of default, base and relative URLs.
40 * New Configuration directives:
41 * ImapMenu {none, formatted, semiformatted, unformatted}
42 * ImapDefault {error, nocontent, referer, menu, URL}
43 * ImapBase {map, referer, URL}
44 * Support for creating non-graphical menu added. (backwards compatible):
45 * Old: directive URL [x,y ...]
46 * New: directive URL "Menu text" [x,y ...]
47 * or: directive URL x,y ... "Menu text"
48 * Map format and menu concept courtesy Joshua Bell, jsbell@acs.ucalgary.ca.
50 * Mark Cox, mark@ukweb.com, Allow relative URLs even when no base specified
54 #include "apr_strings.h"
57 #define APR_WANT_STDIO /* for sscanf() */
58 #define APR_WANT_STRFUNC
61 #include "ap_config.h"
63 #include "http_config.h"
64 #include "http_request.h"
65 #include "http_core.h"
66 #include "http_protocol.h"
67 #include "http_main.h"
69 #include "util_script.h"
73 #define IMAP_MAGIC_TYPE "application/x-httpd-imap"
78 #define IMAP_MENU_DEFAULT "formatted"
79 #define IMAP_DEFAULT_DEFAULT "nocontent"
80 #define IMAP_BASE_DEFAULT "map"
83 double strtod(); /* SunOS needed this */
86 module AP_MODULE_DECLARE_DATA imagemap_module;
94 static void *create_imap_dir_config(apr_pool_t *p, char *dummy)
97 (imap_conf_rec *) apr_palloc(p, sizeof(imap_conf_rec));
99 icr->imap_menu = NULL;
100 icr->imap_default = NULL;
101 icr->imap_base = NULL;
106 static void *merge_imap_dir_configs(apr_pool_t *p, void *basev, void *addv)
108 imap_conf_rec *new = (imap_conf_rec *) apr_pcalloc(p, sizeof(imap_conf_rec));
109 imap_conf_rec *base = (imap_conf_rec *) basev;
110 imap_conf_rec *add = (imap_conf_rec *) addv;
112 new->imap_menu = add->imap_menu ? add->imap_menu : base->imap_menu;
113 new->imap_default = add->imap_default ? add->imap_default
114 : base->imap_default;
115 new->imap_base = add->imap_base ? add->imap_base : base->imap_base;
121 static const command_rec imap_cmds[] =
123 AP_INIT_TAKE1("ImapMenu", ap_set_string_slot,
124 (void *)APR_OFFSETOF(imap_conf_rec, imap_menu), OR_INDEXES,
125 "the type of menu generated: none, formatted, semiformatted, "
127 AP_INIT_TAKE1("ImapDefault", ap_set_string_slot,
128 (void *)APR_OFFSETOF(imap_conf_rec, imap_default), OR_INDEXES,
129 "the action taken if no match: error, nocontent, referer, "
131 AP_INIT_TAKE1("ImapBase", ap_set_string_slot,
132 (void *)APR_OFFSETOF(imap_conf_rec, imap_base), OR_INDEXES,
133 "the base for all URL's: map, referer, URL (or start of)"),
137 static int pointinrect(const double point[2], double coords[MAXVERTS][2])
139 double max[2], min[2];
140 if (coords[0][X] > coords[1][X]) {
141 max[0] = coords[0][X];
142 min[0] = coords[1][X];
145 max[0] = coords[1][X];
146 min[0] = coords[0][X];
149 if (coords[0][Y] > coords[1][Y]) {
150 max[1] = coords[0][Y];
151 min[1] = coords[1][Y];
154 max[1] = coords[1][Y];
155 min[1] = coords[0][Y];
158 return ((point[X] >= min[0] && point[X] <= max[0]) &&
159 (point[Y] >= min[1] && point[Y] <= max[1]));
162 static int pointincircle(const double point[2], double coords[MAXVERTS][2])
164 double radius1, radius2;
166 radius1 = ((coords[0][Y] - coords[1][Y]) * (coords[0][Y] - coords[1][Y]))
167 + ((coords[0][X] - coords[1][X]) * (coords[0][X] - coords[1][X]));
169 radius2 = ((coords[0][Y] - point[Y]) * (coords[0][Y] - point[Y]))
170 + ((coords[0][X] - point[X]) * (coords[0][X] - point[X]));
172 return (radius2 <= radius1);
175 #define fmin(a,b) (((a)>(b))?(b):(a))
176 #define fmax(a,b) (((a)>(b))?(a):(b))
178 static int pointinpoly(const double point[2], double pgon[MAXVERTS][2])
180 int i, numverts, crossings = 0;
181 double x = point[X], y = point[Y];
183 for (numverts = 0; pgon[numverts][X] != -1 && numverts < MAXVERTS;
185 /* just counting the vertexes */
188 for (i = 0; i < numverts; i++) {
189 double x1=pgon[i][X];
190 double y1=pgon[i][Y];
191 double x2=pgon[(i + 1) % numverts][X];
192 double y2=pgon[(i + 1) % numverts][Y];
193 double d=(y - y1) * (x2 - x1) - (x - x1) * (y2 - y1);
195 if ((y1 >= y) != (y2 >= y)) {
196 crossings +=y2 - y1 >= 0 ? d >= 0 : d <= 0;
198 if (!d && fmin(x1,x2) <= x && x <= fmax(x1,x2)
199 && fmin(y1,y2) <= y && y <= fmax(y1,y2)) {
203 return crossings & 0x01;
207 static int is_closer(const double point[2], double coords[MAXVERTS][2],
210 double dist_squared = ((point[X] - coords[0][X])
211 * (point[X] - coords[0][X]))
212 + ((point[Y] - coords[0][Y])
213 * (point[Y] - coords[0][Y]));
215 if (point[X] < 0 || point[Y] < 0) {
216 return (0); /* don't mess around with negative coordinates */
219 if (*closest < 0 || dist_squared < *closest) {
220 *closest = dist_squared;
221 return (1); /* if this is the first point or is the closest yet
222 set 'closest' equal to this distance^2 */
225 return (0); /* if it's not the first or closest */
229 static double get_x_coord(const char *args)
231 char *endptr; /* we want it non-null */
232 double x_coord = -1; /* -1 is returned if no coordinate is given */
235 return (-1); /* in case we aren't passed anything */
238 while (*args && !apr_isdigit(*args) && *args != ',') {
239 args++; /* jump to the first digit, but not past
243 x_coord = strtod(args, &endptr);
245 if (endptr > args) { /* if a conversion was made */
249 return (-1); /* else if no conversion was made,
250 or if no args was given */
253 static double get_y_coord(const char *args)
255 char *endptr; /* we want it non-null */
256 const char *start_of_y = NULL;
257 double y_coord = -1; /* -1 is returned on error */
260 return (-1); /* in case we aren't passed anything */
263 start_of_y = ap_strchr_c(args, ','); /* the comma */
267 start_of_y++; /* start looking at the character after
270 while (*start_of_y && !apr_isdigit(*start_of_y)) {
271 start_of_y++; /* jump to the first digit, but not
275 y_coord = strtod(start_of_y, &endptr);
277 if (endptr > start_of_y) {
282 return (-1); /* if no conversion was made, or
283 no comma was found in args */
287 /* See if string has a "quoted part", and if so set *quoted_part to
288 * the first character of the quoted part, then hammer a \0 onto the
289 * trailing quote, and set *string to point at the first character
290 * past the second quote.
292 * Otherwise set *quoted_part to NULL, and leave *string alone.
294 static void read_quoted(char **string, char **quoted_part)
296 char *strp = *string;
298 /* assume there's no quoted part */
301 while (apr_isspace(*strp)) {
302 strp++; /* go along string until non-whitespace */
305 if (*strp == '"') { /* if that character is a double quote */
306 strp++; /* step over it */
307 *quoted_part = strp; /* note where the quoted part begins */
309 while (*strp && *strp != '"') {
310 ++strp; /* skip the quoted portion */
313 *strp = '\0'; /* end the string with a NUL */
315 strp++; /* step over the last double quote */
321 * returns the mapped URL or NULL.
323 static char *imap_url(request_rec *r, const char *base, const char *value)
325 /* translates a value into a URL. */
327 char *string_pos = NULL;
328 const char *string_pos_const = NULL;
329 char *directory = NULL;
330 const char *referer = NULL;
333 if (!strcasecmp(value, "map") || !strcasecmp(value, "menu")) {
334 return ap_construct_url(r->pool, r->uri, r);
337 if (!strcasecmp(value, "nocontent") || !strcasecmp(value, "error")) {
338 return apr_pstrdup(r->pool, value); /* these are handled elsewhere,
342 if (!strcasecmp(value, "referer")) {
343 referer = apr_table_get(r->headers_in, "Referer");
344 if (referer && *referer) {
345 return ap_escape_html(r->pool, referer);
348 /* XXX: This used to do *value = '\0'; ... which is totally bogus
349 * because it hammers the passed in value, which can be a string
350 * constant, or part of a config, or whatever. Total garbage.
351 * This works around that without changing the rest of this
354 value = ""; /* if 'referer' but no referring page,
359 string_pos_const = value;
360 while (apr_isalpha(*string_pos_const)) {
361 string_pos_const++; /* go along the URL from the map
362 until a non-letter */
364 if (*string_pos_const == ':') {
365 /* if letters and then a colon (like http:) */
366 /* it's an absolute URL, so use it! */
367 return apr_pstrdup(r->pool, value);
370 if (!base || !*base) {
371 if (value && *value) {
372 return apr_pstrdup(r->pool, value); /* no base: use what is given */
374 /* no base, no value: pick a simple default */
375 return ap_construct_url(r->pool, "/", r);
378 /* must be a relative URL to be combined with base */
379 if (ap_strchr_c(base, '/') == NULL && (!strncmp(value, "../", 3)
380 || !strcmp(value, ".."))) {
381 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
382 "invalid base directive in map file: %s", r->uri);
385 my_base = apr_pstrdup(r->pool, base);
386 string_pos = my_base;
387 while (*string_pos) {
388 if (*string_pos == '/' && *(string_pos + 1) == '/') {
389 string_pos += 2; /* if there are two slashes, jump over them */
392 if (*string_pos == '/') { /* the first single slash */
393 if (value[0] == '/') {
395 } /* if the URL from the map starts from root,
396 end the base URL string at the first single
399 directory = string_pos; /* save the start of
400 the directory portion */
402 string_pos = strrchr(string_pos, '/'); /* now reuse
404 string_pos++; /* step over that last slash */
406 } /* but if the map url is relative, leave the
407 slash on the base (if there is one) */
410 string_pos++; /* until we get to the end of my_base without
411 finding a slash by itself */
414 while (!strncmp(value, "../", 3) || !strcmp(value, "..")) {
416 if (directory && (slen = strlen(directory))) {
418 /* for each '..', knock a directory off the end
419 by ending the string right at the last slash.
420 But only consider the directory portion: don't eat
421 into the server name. And only try if a directory
426 while ((slen - clen) == 1) {
428 if ((string_pos = strrchr(directory, '/'))) {
431 clen = strlen(directory);
437 value += 2; /* jump over the '..' that we found in the
440 else if (directory) {
441 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
442 "invalid directory name in map file: %s", r->uri);
446 if (!strncmp(value, "/../", 4) || !strcmp(value, "/..")) {
447 value++; /* step over the '/' if there are more '..'
448 to do. This way, we leave the starting
449 '/' on value after the last '..', but get
450 rid of it otherwise */
453 } /* by this point, value does not start
456 if (value && *value) {
457 return apr_pstrcat(r->pool, my_base, value, NULL);
462 static int imap_reply(request_rec *r, char *redirect)
464 if (!strcasecmp(redirect, "error")) {
465 /* they actually requested an error! */
466 return HTTP_INTERNAL_SERVER_ERROR;
468 if (!strcasecmp(redirect, "nocontent")) {
469 /* tell the client to keep the page it has */
470 return HTTP_NO_CONTENT;
472 if (redirect && *redirect) {
473 /* must be a URL, so redirect to it */
474 apr_table_setn(r->headers_out, "Location", redirect);
475 return HTTP_MOVED_TEMPORARILY;
477 return HTTP_INTERNAL_SERVER_ERROR;
480 static void menu_header(request_rec *r, char *menu)
482 ap_set_content_type(r, "text/html");
484 ap_rvputs(r, DOCTYPE_HTML_3_2, "<html><head>\n<title>Menu for ", r->uri,
485 "</title>\n</head><body>\n", NULL);
487 if (!strcasecmp(menu, "formatted")) {
488 ap_rvputs(r, "<h1>Menu for ", r->uri, "</h1>\n<hr />\n\n", NULL);
494 static void menu_blank(request_rec *r, char *menu)
496 if (!strcasecmp(menu, "formatted")) {
499 if (!strcasecmp(menu, "semiformatted")) {
500 ap_rputs("<br />\n", r);
502 if (!strcasecmp(menu, "unformatted")) {
508 static void menu_comment(request_rec *r, char *menu, char *comment)
510 if (!strcasecmp(menu, "formatted")) {
511 ap_rputs("\n", r); /* print just a newline if 'formatted' */
513 if (!strcasecmp(menu, "semiformatted") && *comment) {
514 ap_rvputs(r, comment, "\n", NULL);
516 if (!strcasecmp(menu, "unformatted") && *comment) {
517 ap_rvputs(r, comment, "\n", NULL);
519 return; /* comments are ignored in the
523 static void menu_default(request_rec *r, char *menu, char *href, char *text)
525 if (!strcasecmp(href, "error") || !strcasecmp(href, "nocontent")) {
526 return; /* don't print such lines, these aren't
529 if (!strcasecmp(menu, "formatted")) {
530 ap_rvputs(r, "<pre>(Default) <a href=\"", href, "\">", text,
531 "</a></pre>\n", NULL);
533 if (!strcasecmp(menu, "semiformatted")) {
534 ap_rvputs(r, "<pre>(Default) <a href=\"", href, "\">", text,
535 "</a></pre>\n", NULL);
537 if (!strcasecmp(menu, "unformatted")) {
538 ap_rvputs(r, "<a href=\"", href, "\">", text, "</a>", NULL);
543 static void menu_directive(request_rec *r, char *menu, char *href, char *text)
545 if (!strcasecmp(href, "error") || !strcasecmp(href, "nocontent")) {
546 return; /* don't print such lines, as this isn't
549 if (!strcasecmp(menu, "formatted")) {
550 ap_rvputs(r, "<pre> <a href=\"", href, "\">", text,
551 "</a></pre>\n", NULL);
553 if (!strcasecmp(menu, "semiformatted")) {
554 ap_rvputs(r, "<pre> <a href=\"", href, "\">", text,
555 "</a></pre>\n", NULL);
557 if (!strcasecmp(menu, "unformatted")) {
558 ap_rvputs(r, "<a href=\"", href, "\">", text, "</a>", NULL);
563 static void menu_footer(request_rec *r)
565 ap_rputs("\n\n</body>\n</html>\n", r); /* finish the menu */
568 static int imap_handler_internal(request_rec *r)
570 char input[MAX_STRING_LEN];
577 char *closest = NULL;
578 double closest_yet = -1;
582 double pointarray[MAXVERTS + 1][2];
594 ap_configfile_t *imap;
596 icr = ap_get_module_config(r->per_dir_config, &imagemap_module);
598 imap_menu = icr->imap_menu ? icr->imap_menu : IMAP_MENU_DEFAULT;
599 imap_default = icr->imap_default
600 ? icr->imap_default : IMAP_DEFAULT_DEFAULT;
601 imap_base = icr->imap_base ? icr->imap_base : IMAP_BASE_DEFAULT;
603 status = ap_pcfg_openfile(&imap, r->pool, r->filename);
605 if (status != APR_SUCCESS) {
606 return HTTP_NOT_FOUND;
609 base = imap_url(r, NULL, imap_base); /* set base according
612 return HTTP_INTERNAL_SERVER_ERROR;
614 mapdflt = imap_url(r, NULL, imap_default); /* and default to
617 return HTTP_INTERNAL_SERVER_ERROR;
620 testpoint[X] = get_x_coord(r->args);
621 testpoint[Y] = get_y_coord(r->args);
623 if ((testpoint[X] == -1 || testpoint[Y] == -1) ||
624 (testpoint[X] == 0 && testpoint[Y] == 0)) {
625 /* if either is -1 or if both are zero (new Lynx) */
626 /* we don't have valid coordinates */
629 if (strncasecmp(imap_menu, "none", 2)) {
630 showmenu = 1; /* show the menu _unless_ ImapMenu is
635 if (showmenu) { /* send start of imagemap menu if
637 menu_header(r, imap_menu);
640 while (!ap_cfg_getline(input, sizeof(input), imap)) {
643 menu_blank(r, imap_menu);
648 if (input[0] == '#') {
650 menu_comment(r, imap_menu, input + 1);
653 } /* blank lines and comments are ignored
654 if we aren't printing a menu */
656 /* find the first two space delimited fields, recall that
657 * ap_cfg_getline has removed leading/trailing whitespace.
659 * note that we're tokenizing as we go... if we were to use the
660 * ap_getword() class of functions we would end up allocating extra
661 * memory for every line of the map file
664 if (!*string_pos) { /* need at least two fields */
668 directive = string_pos;
669 while (*string_pos && !apr_isspace(*string_pos)) { /* past directive */
672 if (!*string_pos) { /* need at least two fields */
675 *string_pos++ = '\0';
677 if (!*string_pos) { /* need at least two fields */
680 while(*string_pos && apr_isspace(*string_pos)) { /* past whitespace */
685 while (*string_pos && !apr_isspace(*string_pos)) { /* past value */
688 if (apr_isspace(*string_pos)) {
689 *string_pos++ = '\0';
692 /* end of input, don't advance past it */
696 if (!strncasecmp(directive, "base", 4)) { /* base, base_uri */
697 base = imap_url(r, NULL, value);
701 continue; /* base is never printed to a menu */
704 read_quoted(&string_pos, &href_text);
706 if (!strcasecmp(directive, "default")) { /* default */
707 mapdflt = imap_url(r, NULL, value);
711 if (showmenu) { /* print the default if there's a menu */
712 redirect = imap_url(r, base, mapdflt);
716 menu_default(r, imap_menu, redirect,
717 href_text ? href_text : mapdflt);
723 while (vertex < MAXVERTS &&
724 sscanf(string_pos, "%lf%*[, ]%lf",
725 &pointarray[vertex][X], &pointarray[vertex][Y]) == 2) {
726 /* Now skip what we just read... we can't use ANSIism %n */
727 while (apr_isspace(*string_pos)) { /* past whitespace */
730 while (apr_isdigit(*string_pos)) { /* and the 1st number */
733 string_pos++; /* skip the ',' */
734 while (apr_isspace(*string_pos)) { /* past any more whitespace */
737 while (apr_isdigit(*string_pos)) { /* 2nd number */
741 } /* so long as there are more vertices to
742 read, and we have room, read them in.
743 We start where we left off of the last
744 sscanf, not at the beginning. */
746 pointarray[vertex][X] = -1; /* signals the end of vertices */
750 read_quoted(&string_pos, &href_text); /* href text could
753 redirect = imap_url(r, base, value);
757 menu_directive(r, imap_menu, redirect,
758 href_text ? href_text : value);
761 /* note that we don't make it past here if we are making a menu */
763 if (testpoint[X] == -1 || pointarray[0][X] == -1) {
764 continue; /* don't try the following tests if testpoints
765 are invalid, or if there are no
769 if (!strcasecmp(directive, "poly")) { /* poly */
771 if (pointinpoly(testpoint, pointarray)) {
772 ap_cfg_closefile(imap);
773 redirect = imap_url(r, base, value);
775 return HTTP_INTERNAL_SERVER_ERROR;
777 return (imap_reply(r, redirect));
782 if (!strcasecmp(directive, "circle")) { /* circle */
784 if (pointincircle(testpoint, pointarray)) {
785 ap_cfg_closefile(imap);
786 redirect = imap_url(r, base, value);
788 return HTTP_INTERNAL_SERVER_ERROR;
790 return (imap_reply(r, redirect));
795 if (!strcasecmp(directive, "rect")) { /* rect */
797 if (pointinrect(testpoint, pointarray)) {
798 ap_cfg_closefile(imap);
799 redirect = imap_url(r, base, value);
801 return HTTP_INTERNAL_SERVER_ERROR;
803 return (imap_reply(r, redirect));
808 if (!strcasecmp(directive, "point")) { /* point */
810 if (is_closer(testpoint, pointarray, &closest_yet)) {
811 closest = apr_pstrdup(r->pool, value);
815 } /* move on to next line whether it's
818 } /* nothing matched, so we get another line! */
820 ap_cfg_closefile(imap); /* we are done with the map file; close it */
823 menu_footer(r); /* finish the menu and we are done */
827 if (closest) { /* if a 'point' directive has been seen */
828 redirect = imap_url(r, base, closest);
830 return HTTP_INTERNAL_SERVER_ERROR;
832 return (imap_reply(r, redirect));
835 if (mapdflt) { /* a default should be defined, even if
837 redirect = imap_url(r, base, mapdflt);
839 return HTTP_INTERNAL_SERVER_ERROR;
841 return (imap_reply(r, redirect));
844 return HTTP_INTERNAL_SERVER_ERROR; /* If we make it this far,
845 we failed. They lose! */
848 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
849 "map file %s, line %d syntax error: requires at "
850 "least two fields", r->uri, imap->line_number);
853 ap_cfg_closefile(imap);
855 /* There's not much else we can do ... we've already sent the headers
858 ap_rputs("\n\n[an internal server error occured]\n", r);
862 return HTTP_INTERNAL_SERVER_ERROR;
865 static int imap_handler(request_rec *r)
867 /* Optimization: skip the allocation of large local variables on the
868 * stack (in imap_handler_internal()) on requests that aren't using
871 if (r->method_number != M_GET || (strcmp(r->handler,IMAP_MAGIC_TYPE)
872 && strcmp(r->handler, "imap-file"))) {
876 return imap_handler_internal(r);
880 static void register_hooks(apr_pool_t *p)
882 ap_hook_handler(imap_handler,NULL,NULL,APR_HOOK_MIDDLE);
885 module AP_MODULE_DECLARE_DATA imagemap_module =
887 STANDARD20_MODULE_STUFF,
888 create_imap_dir_config, /* dir config creater */
889 merge_imap_dir_configs, /* dir merger --- default is to override */
890 NULL, /* server config */
891 NULL, /* merge server config */
892 imap_cmds, /* command apr_table_t */
893 register_hooks /* register hooks */