]> sjero.net Git - wget/blob - src/html-url.c
[svn] Committed a bunch of different tweaks of mine.
[wget] / src / html-url.c
1 /* Collect URLs from HTML source.
2    Copyright (C) 1998, 2000 Free Software Foundation, Inc.
3
4 This file is part of Wget.
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
19
20 #include <config.h>
21
22 #include <stdio.h>
23 #ifdef HAVE_STRING_H
24 # include <string.h>
25 #else
26 # include <strings.h>
27 #endif
28 #include <stdlib.h>
29 #include <ctype.h>
30 #include <errno.h>
31 #include <assert.h>
32
33 #include "wget.h"
34 #include "html-parse.h"
35 #include "url.h"
36 #include "utils.h"
37
38 #ifndef errno
39 extern int errno;
40 #endif
41
42 enum tag_category { TC_LINK, TC_SPEC };
43
44 /* Here we try to categorize the known tags.  Each tag has its ID and
45    cetegory.  Category TC_LINK means that one or more of its
46    attributes contain links that should be retrieved.  TC_SPEC means
47    that the tag is specific in some way, and has to be handled
48    specially. */
49 static struct {
50   const char *name;
51   enum tag_category category;
52 } known_tags[] = {
53 #define TAG_A           0
54   { "a",        TC_LINK },
55 #define TAG_APPLET      1
56   { "applet",   TC_LINK },
57 #define TAG_AREA        2
58   { "area",     TC_LINK },
59 #define TAG_BASE        3
60   { "base",     TC_SPEC },
61 #define TAG_BGSOUND     4
62   { "bgsound",  TC_LINK },
63 #define TAG_BODY        5
64   { "body",     TC_LINK },
65 #define TAG_EMBED       6
66   { "embed",    TC_LINK },
67 #define TAG_FIG         7
68   { "fig",      TC_LINK },
69 #define TAG_FRAME       8
70   { "frame",    TC_LINK },
71 #define TAG_IFRAME      9
72   { "iframe",   TC_LINK },
73 #define TAG_IMG         10
74   { "img",      TC_LINK },
75 #define TAG_INPUT       11
76   { "input",    TC_LINK },
77 #define TAG_LAYER       12
78   { "layer",    TC_LINK },
79 #define TAG_LINK        13
80   { "link",     TC_SPEC },
81 #define TAG_META        14
82   { "meta",     TC_SPEC },
83 #define TAG_OVERLAY     15
84   { "overlay",  TC_LINK },
85 #define TAG_SCRIPT      16
86   { "script",   TC_LINK },
87 #define TAG_TABLE       17
88   { "table",    TC_LINK },
89 #define TAG_TD          18
90   { "td",       TC_LINK },
91 #define TAG_TH          19
92   { "th",       TC_LINK }
93 };
94
95 /* Flags for specific url-attr pairs handled through TC_LINK: */
96 #define AF_EXTERNAL 1
97
98 /* For tags handled by TC_LINK: attributes that contain URLs to
99    download. */
100 static struct {
101   int tagid;
102   const char *attr_name;
103   int flags;
104 } url_tag_attr_map[] = {
105   { TAG_A,              "href",         AF_EXTERNAL },
106   { TAG_APPLET,         "code",         0 },
107   { TAG_AREA,           "href",         AF_EXTERNAL },
108   { TAG_BGSOUND,        "src",          0 },
109   { TAG_BODY,           "background",   0 },
110   { TAG_EMBED,          "src",          0 },
111   { TAG_FIG,            "src",          0 },
112   { TAG_FRAME,          "src",          0 },
113   { TAG_IFRAME,         "src",          0 },
114   { TAG_IMG,            "href",         0 },
115   { TAG_IMG,            "lowsrc",       0 },
116   { TAG_IMG,            "src",          0 },
117   { TAG_INPUT,          "src",          0 },
118   { TAG_LAYER,          "src",          0 },
119   { TAG_OVERLAY,        "src",          0 },
120   { TAG_SCRIPT,         "src",          0 },
121   { TAG_TABLE,          "background",   0 },
122   { TAG_TD,             "background",   0 },
123   { TAG_TH,             "background",   0 }
124 };
125
126 /* The lists of interesting tags and attributes are built dynamically,
127    from the information above.  However, some places in the code refer
128    to the attributes not mentioned here.  We add them manually.  */
129 static const char *additional_attributes[] = {
130   "rel",                        /* for TAG_LINK */
131   "http-equiv",                 /* for TAG_META */
132   "name",                       /* for TAG_META */
133   "content"                     /* for TAG_META */
134 };
135
136 static const char **interesting_tags;
137 static const char **interesting_attributes;
138
139 void
140 init_interesting (void)
141 {
142   /* Init the variables interesting_tags and interesting_attributes
143      that are used by the HTML parser to know which tags and
144      attributes we're interested in.  We initialize this only once,
145      for performance reasons.
146
147      Here we also make sure that what we put in interesting_tags
148      matches the user's preferences as specified through --ignore-tags
149      and --follow-tags.  */
150
151   {
152     int i, ind = 0;
153     int size = ARRAY_SIZE (known_tags);
154     interesting_tags = (const char **)xmalloc ((size + 1) * sizeof (char *));
155
156     for (i = 0; i < size; i++)
157       {
158         const char *name = known_tags[i].name;
159
160         /* Normally here we could say:
161            interesting_tags[i] = name;
162            But we need to respect the settings of --ignore-tags and
163            --follow-tags, so the code gets a bit harier.  */
164
165         if (opt.ignore_tags)
166           {
167             /* --ignore-tags was specified.  Do not match these
168                specific tags.  --ignore-tags takes precedence over
169                --follow-tags, so we process --ignore first and fall
170                through if there's no match. */
171             int j, lose = 0;
172             for (j = 0; opt.ignore_tags[j] != NULL; j++)
173               /* Loop through all the tags this user doesn't care
174                  about. */
175               if (strcasecmp(opt.ignore_tags[j], name) == EQ)
176                 {
177                   lose = 1;
178                   break;
179                 }
180             if (lose)
181               continue;
182           }
183
184         if (opt.follow_tags)
185           {
186             /* --follow-tags was specified.  Only match these specific
187                tags, so return FALSE if we don't match one of them. */
188             int j, win = 0;
189             for (j = 0; opt.follow_tags[j] != NULL; j++)
190               /* Loop through all the tags this user cares about. */
191               if (strcasecmp(opt.follow_tags[j], name) == EQ)
192                 {
193                   win = 1;
194                   break;
195                 }
196             if (!win)
197               continue;         /* wasn't one of the explicitly
198                                    desired tags */
199           }
200
201         /* If we get to here, --follow-tags isn't being used or the
202            tag is among the ones that are follwed, and --ignore-tags,
203            if specified, didn't include this tag, so it's an
204            "interesting" one. */
205         interesting_tags[ind++] = name;
206       }
207     interesting_tags[ind] = NULL;
208   }
209
210   /* The same for attributes, except we loop through url_tag_attr_map.
211      Here we also need to make sure that the list of attributes is
212      unique, and to include the attributes from additional_attributes.  */
213   {
214     int i, ind;
215     const char **att = xmalloc ((ARRAY_SIZE (additional_attributes) + 1)
216                                 * sizeof (char *));
217     /* First copy the "additional" attributes. */
218     for (i = 0; i < ARRAY_SIZE (additional_attributes); i++)
219       att[i] = additional_attributes[i];
220     ind = i;
221     att[ind] = NULL;
222     for (i = 0; i < ARRAY_SIZE (url_tag_attr_map); i++)
223       {
224         int j, seen = 0;
225         const char *look_for = url_tag_attr_map[i].attr_name;
226         for (j = 0; j < ind - 1; j++)
227           if (!strcmp (att[j], look_for))
228             {
229               seen = 1;
230               break;
231             }
232         if (!seen)
233           {
234             att = xrealloc (att, (ind + 2) * sizeof (*att));
235             att[ind++] = look_for;
236             att[ind] = NULL;
237           }
238       }
239     interesting_attributes = att;
240   }
241 }
242
243 static int
244 find_tag (const char *tag_name)
245 {
246   int i;
247
248   /* This is linear search; if the number of tags grow, we can switch
249      to binary search.  */
250
251   for (i = 0; i < ARRAY_SIZE (known_tags); i++)
252     {
253       int cmp = strcasecmp (known_tags[i].name, tag_name);
254       /* known_tags are sorted alphabetically, so we can
255          micro-optimize.  */
256       if (cmp > 0)
257         break;
258       else if (cmp == 0)
259         return i;
260     }
261   return -1;
262 }
263
264 /* Find the value of attribute named NAME in the taginfo TAG.  If the
265    attribute is not present, return NULL.  If ATTRID is non-NULL, the
266    exact identity of the attribute will be returned.  */
267 static char *
268 find_attr (struct taginfo *tag, const char *name, int *attrid)
269 {
270   int i;
271   for (i = 0; i < tag->nattrs; i++)
272     if (!strcasecmp (tag->attrs[i].name, name))
273       {
274         if (attrid)
275           *attrid = i;
276         return tag->attrs[i].value;
277       }
278   return NULL;
279 }
280
281 struct collect_urls_closure {
282   char *text;                   /* HTML text. */
283   char *base;                   /* Base URI of the document, possibly
284                                    changed through <base href=...>. */
285   urlpos *head, *tail;          /* List of URLs */
286   const char *parent_base;      /* Base of the current document. */
287   const char *document_file;    /* File name of this document. */
288   int dash_p_leaf_HTML;         /* Whether -p is specified, and this
289                                    document is the "leaf" node of the
290                                    HTML tree. */
291   int nofollow;                 /* whether NOFOLLOW was specified in a
292                                    <meta name=robots> tag. */
293 };
294
295 /* Resolve LINK_URI and append it to closure->tail.  TAG and ATTRID
296    are the necessary context to store the position and size.  */
297
298 static void
299 handle_link (struct collect_urls_closure *closure, const char *link_uri,
300              struct taginfo *tag, int attrid)
301 {
302   int no_proto = !has_proto (link_uri);
303   urlpos *newel;
304
305   const char *base = closure->base ? closure->base : closure->parent_base;
306   char *complete_uri;
307
308   char *fragment = strrchr (link_uri, '#');
309
310   if (fragment)
311     {
312       /* Nullify the fragment identifier, i.e. everything after the
313          last occurrence of `#', inclusive.  This copying is
314          relatively inefficient, but it doesn't matter because
315          fragment identifiers don't come up all that often.  */
316       int hashlen = fragment - link_uri;
317       char *p = alloca (hashlen + 1);
318       memcpy (p, link_uri, hashlen);
319       p[hashlen] = '\0';
320       link_uri = p;
321     }
322
323   if (!base)
324     {
325       if (no_proto)
326         {
327           /* We have no base, and the link does not have a protocol or
328              a host attached to it.  Nothing we can do.  */
329           /* #### Should we print a warning here?  Wget 1.5.x used to.  */
330           return;
331         }
332       else
333         complete_uri = xstrdup (link_uri);
334     }
335   else
336     complete_uri = url_concat (base, link_uri);
337
338   DEBUGP (("%s: merge(\"%s\", \"%s\") -> %s\n",
339            closure->document_file, base ? base : "(null)",
340            link_uri, complete_uri));
341
342   newel = (urlpos *)xmalloc (sizeof (urlpos));
343
344   memset (newel, 0, sizeof (*newel));
345   newel->next = NULL;
346   newel->url = complete_uri;
347   newel->pos = tag->attrs[attrid].value_raw_beginning - closure->text;
348   newel->size = tag->attrs[attrid].value_raw_size;
349
350   /* A URL is relative if the host and protocol are not named, and the
351      name does not start with `/'.  */
352   if (no_proto && *link_uri != '/')
353     newel->link_relative_p = 1;
354   else if (!no_proto)
355     newel->link_complete_p = 1;
356
357   if (closure->tail)
358     {
359       closure->tail->next = newel;
360       closure->tail = newel;
361     }
362   else
363     closure->tail = closure->head = newel;
364 }
365
366 /* #### Document what this does.
367    #### It would be nice to split this into several functions.  */
368
369 static void
370 collect_tags_mapper (struct taginfo *tag, void *arg)
371 {
372   struct collect_urls_closure *closure = (struct collect_urls_closure *)arg;
373   int tagid = find_tag (tag->name);
374   assert (tagid != -1);
375
376   switch (known_tags[tagid].category)
377     {
378     case TC_LINK:
379       {
380         int i;
381         int size = ARRAY_SIZE (url_tag_attr_map);
382         for (i = 0; i < size; i++)
383           if (url_tag_attr_map[i].tagid == tagid)
384             break;
385         /* We've found the index of url_tag_attr_map where the
386            attributes of our tags begin.  Now, look for every one of
387            them, and handle it.  */
388         for (; (i < size && url_tag_attr_map[i].tagid == tagid); i++)
389           {
390             char *attr_value;
391             int id;
392             if (closure->dash_p_leaf_HTML
393                 && (url_tag_attr_map[i].flags & AF_EXTERNAL))
394               /* If we're at a -p leaf node, we don't want to retrieve
395                  links to references we know are external, such as <a
396                  href=...>.  */
397               continue;
398
399             /* This find_attr() buried in a loop may seem inefficient
400                (O(n^2)), but it's not, since the number of attributes
401                (n) we loop over is extremely small.  In the worst case
402                of IMG with all its possible attributes, n^2 will be
403                only 9.  */
404             attr_value = find_attr (tag, url_tag_attr_map[i].attr_name, &id);
405             if (attr_value)
406               handle_link (closure, attr_value, tag, id);
407           }
408       }
409       break;
410     case TC_SPEC:
411       switch (tagid)
412         {
413         case TAG_BASE:
414           {
415             char *newbase = find_attr (tag, "href", NULL);
416             if (!newbase)
417               break;
418             if (closure->base)
419               free (closure->base);
420             if (closure->parent_base)
421               closure->base = url_concat (closure->parent_base, newbase);
422             else
423               closure->base = xstrdup (newbase);
424           }
425           break;
426         case TAG_LINK:
427           {
428             int id;
429             char *rel  = find_attr (tag, "rel", NULL);
430             char *href = find_attr (tag, "href", &id);
431             if (href)
432               {
433                 /* In the normal case, all <link href=...> tags are
434                    fair game.
435
436                    In the special case of when -p is active, however,
437                    and we're at a leaf node (relative to the -l
438                    max. depth) in the HTML document tree, the only
439                    <LINK> tag we'll follow is a <LINK REL=
440                    "stylesheet">, as it's necessary for displaying
441                    this document properly.  We won't follow other
442                    <LINK> tags, like <LINK REL="home">, for instance,
443                    as they refer to external documents.  */
444                 if (!closure->dash_p_leaf_HTML
445                     || (rel && !strcasecmp (rel, "stylesheet")))
446                   handle_link (closure, href, tag, id);
447               }
448           }
449           break;
450         case TAG_META:
451           /* Some pages use a META tag to specify that the page be
452              refreshed by a new page after a given number of seconds.
453              The general format for this is:
454
455              <meta http-equiv=Refresh content="NUMBER; URL=index2.html">
456
457              So we just need to skip past the "NUMBER; URL=" garbage
458              to get to the URL.  */
459           {
460             int id;
461             char *name = find_attr (tag, "name", NULL);
462             char *http_equiv = find_attr (tag, "http-equiv", &id);
463             if (http_equiv && !strcasecmp (http_equiv, "refresh"))
464               {
465                 char *refresh = find_attr (tag, "content", NULL);
466                 char *p = refresh;
467                 int offset;
468                 while (ISDIGIT (*p))
469                   ++p;
470                 if (*p++ != ';')
471                   return;
472                 while (ISSPACE (*p))
473                   ++p;
474                 if (!(TOUPPER (*p) == 'U'
475                       && TOUPPER (*(p + 1)) == 'R'
476                       && TOUPPER (*(p + 2)) == 'L'
477                       && *(p + 3) == '='))
478                   return;
479                 p += 4;
480                 while (ISSPACE (*p))
481                   ++p;
482                 offset = p - refresh;
483                 tag->attrs[id].value_raw_beginning += offset;
484                 tag->attrs[id].value_raw_size -= offset;
485                 handle_link (closure, p, tag, id);
486               }
487             else if (name && !strcasecmp (name, "robots"))
488               {
489                 /* Handle stuff like:
490                    <meta name="robots" content="index,nofollow"> */
491                 char *content = find_attr (tag, "content", NULL);
492                 if (!content)
493                   return;
494                 if (!strcasecmp (content, "none"))
495                   closure->nofollow = 1;
496                 else
497                   {
498                     while (*content)
499                       {
500                         /* Find the next occurrence of ',' or the end of
501                            the string.  */
502                         char *end = strchr (content, ',');
503                         if (end)
504                           ++end;
505                         else
506                           end = content + strlen (content);
507                         if (!strncasecmp (content, "nofollow", end - content))
508                           closure->nofollow = 1;
509                         content = end;
510                       }
511                   }
512               }
513           }
514           break;
515         default:
516           /* Category is TC_SPEC, but tag name is unhandled.  This
517              must not be.  */
518           abort ();
519         }
520       break;
521     }
522 }
523
524 /* Scan FILE, retrieving links to HTML documents from it.  Each link is 
525
526   Similar to get_urls_file, but for HTML files.  FILE is scanned as
527    an HTML document.  get_urls_html() constructs the URLs from the
528    relative href-s.
529
530    If SILENT is non-zero, do not barf on baseless relative links.  */
531 urlpos *
532 get_urls_html (const char *file, const char *this_url, int dash_p_leaf_HTML,
533                int *meta_disallow_follow)
534 {
535   struct file_memory *fm;
536   struct collect_urls_closure closure;
537
538   /* Load the file. */
539   fm = read_file (file);
540   if (!fm)
541     {
542       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
543       return NULL;
544     }
545   DEBUGP (("Loaded %s (size %ld).\n", file, fm->length));
546
547   closure.text = fm->content;
548   closure.head = closure.tail = NULL;
549   closure.base = NULL;
550   closure.parent_base = this_url ? this_url : opt.base_href;
551   closure.document_file = file;
552   closure.dash_p_leaf_HTML = dash_p_leaf_HTML;
553   closure.nofollow = 0;
554
555   if (!interesting_tags)
556     init_interesting ();
557
558   map_html_tags (fm->content, fm->length, interesting_tags,
559                  interesting_attributes, collect_tags_mapper, &closure);
560
561   DEBUGP (("no-follow in %s: %d\n", file, closure.nofollow));
562   if (meta_disallow_follow)
563     *meta_disallow_follow = closure.nofollow;
564
565   FREE_MAYBE (closure.base);
566   read_file_free (fm);
567   return closure.head;
568 }