]> sjero.net Git - wget/blob - src/html-url.c
[svn] A bunch of new features:
[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      #### This logic might need some rethinking.  */
353   if (no_proto && *link_uri != '/')
354     newel->flags |= (URELATIVE | UNOPROTO);
355   else if (no_proto)
356     newel->flags |= UNOPROTO;
357
358   if (closure->tail)
359     {
360       closure->tail->next = newel;
361       closure->tail = newel;
362     }
363   else
364     closure->tail = closure->head = newel;
365 }
366
367 /* #### Document what this does.
368    #### It would be nice to split this into several functions.  */
369
370 static void
371 collect_tags_mapper (struct taginfo *tag, void *arg)
372 {
373   struct collect_urls_closure *closure = (struct collect_urls_closure *)arg;
374   int tagid = find_tag (tag->name);
375   assert (tagid != -1);
376
377   switch (known_tags[tagid].category)
378     {
379     case TC_LINK:
380       {
381         int i;
382         int size = ARRAY_SIZE (url_tag_attr_map);
383         for (i = 0; i < size; i++)
384           if (url_tag_attr_map[i].tagid == tagid)
385             break;
386         /* We've found the index of url_tag_attr_map where the
387            attributes of our tags begin.  Now, look for every one of
388            them, and handle it.  */
389         for (; (i < size && url_tag_attr_map[i].tagid == tagid); i++)
390           {
391             char *attr_value;
392             int id;
393             if (closure->dash_p_leaf_HTML
394                 && (url_tag_attr_map[i].flags & AF_EXTERNAL))
395               /* If we're at a -p leaf node, we don't want to retrieve
396                  links to references we know are external, such as <a
397                  href=...>.  */
398               continue;
399
400             /* This find_attr() buried in a loop may seem inefficient
401                (O(n^2)), but it's not, since the number of attributes
402                (n) we loop over is extremely small.  In the worst case
403                of IMG with all its possible attributes, n^2 will be
404                only 9.  */
405             attr_value = find_attr (tag, url_tag_attr_map[i].attr_name, &id);
406             if (attr_value)
407               handle_link (closure, attr_value, tag, id);
408           }
409       }
410       break;
411     case TC_SPEC:
412       switch (tagid)
413         {
414         case TAG_BASE:
415           {
416             char *newbase = find_attr (tag, "href", NULL);
417             if (!newbase)
418               break;
419             if (closure->base)
420               free (closure->base);
421             if (closure->parent_base)
422               closure->base = url_concat (closure->parent_base, newbase);
423             else
424               closure->base = xstrdup (newbase);
425           }
426           break;
427         case TAG_LINK:
428           {
429             int id;
430             char *rel  = find_attr (tag, "rel", NULL);
431             char *href = find_attr (tag, "href", &id);
432             if (href)
433               {
434                 /* In the normal case, all <link href=...> tags are
435                    fair game.
436
437                    In the special case of when -p is active, however,
438                    and we're at a leaf node (relative to the -l
439                    max. depth) in the HTML document tree, the only
440                    <LINK> tag we'll follow is a <LINK REL=
441                    "stylesheet">, as it's necessary for displaying
442                    this document properly.  We won't follow other
443                    <LINK> tags, like <LINK REL="home">, for instance,
444                    as they refer to external documents.  */
445                 if (!closure->dash_p_leaf_HTML
446                     || (rel && !strcasecmp (rel, "stylesheet")))
447                   handle_link (closure, href, tag, id);
448               }
449           }
450           break;
451         case TAG_META:
452           /* Some pages use a META tag to specify that the page be
453              refreshed by a new page after a given number of seconds.
454              The general format for this is:
455
456              <meta http-equiv=Refresh content="NUMBER; URL=index2.html">
457
458              So we just need to skip past the "NUMBER; URL=" garbage
459              to get to the URL.  */
460           {
461             int id;
462             char *name = find_attr (tag, "name", NULL);
463             char *http_equiv = find_attr (tag, "http-equiv", &id);
464             if (http_equiv && !strcasecmp (http_equiv, "refresh"))
465               {
466                 char *refresh = find_attr (tag, "content", NULL);
467                 char *p = refresh;
468                 int offset;
469                 while (ISDIGIT (*p))
470                   ++p;
471                 if (*p++ != ';')
472                   return;
473                 while (ISSPACE (*p))
474                   ++p;
475                 if (!(TOUPPER (*p) == 'U'
476                       && TOUPPER (*(p + 1)) == 'R'
477                       && TOUPPER (*(p + 2)) == 'L'
478                       && *(p + 3) == '='))
479                   return;
480                 p += 4;
481                 while (ISSPACE (*p))
482                   ++p;
483                 offset = p - refresh;
484                 tag->attrs[id].value_raw_beginning += offset;
485                 tag->attrs[id].value_raw_size -= offset;
486                 handle_link (closure, p, tag, id);
487               }
488             else if (name && !strcasecmp (name, "robots"))
489               {
490                 /* Handle stuff like:
491                    <meta name="robots" content="index,nofollow"> */
492                 char *content = find_attr (tag, "content", NULL);
493                 if (!content)
494                   return;
495                 if (!strcasecmp (content, "none"))
496                   closure->nofollow = 1;
497                 else
498                   {
499                     while (*content)
500                       {
501                         /* Find the next occurrence of ',' or the end of
502                            the string.  */
503                         char *end = strchr (content, ',');
504                         if (end)
505                           ++end;
506                         else
507                           end = content + strlen (content);
508                         if (!strncasecmp (content, "nofollow", end - content))
509                           closure->nofollow = 1;
510                         content = end;
511                       }
512                   }
513               }
514           }
515           break;
516         default:
517           /* Category is TC_SPEC, but tag name is unhandled.  This
518              must not be.  */
519           abort ();
520         }
521       break;
522     }
523 }
524
525 /* Scan FILE, retrieving links to HTML documents from it.  Each link is 
526
527   Similar to get_urls_file, but for HTML files.  FILE is scanned as
528    an HTML document.  get_urls_html() constructs the URLs from the
529    relative href-s.
530
531    If SILENT is non-zero, do not barf on baseless relative links.  */
532 urlpos *
533 get_urls_html (const char *file, const char *this_url, int dash_p_leaf_HTML,
534                int *meta_disallow_follow)
535 {
536   struct file_memory *fm;
537   struct collect_urls_closure closure;
538
539   /* Load the file. */
540   fm = read_file (file);
541   if (!fm)
542     {
543       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
544       return NULL;
545     }
546   DEBUGP (("Loaded %s (size %ld).\n", file, fm->length));
547
548   closure.text = fm->content;
549   closure.head = closure.tail = NULL;
550   closure.base = NULL;
551   closure.parent_base = this_url ? this_url : opt.base_href;
552   closure.document_file = file;
553   closure.dash_p_leaf_HTML = dash_p_leaf_HTML;
554   closure.nofollow = 0;
555
556   if (!interesting_tags)
557     init_interesting ();
558
559   map_html_tags (fm->content, fm->length, interesting_tags,
560                  interesting_attributes, collect_tags_mapper, &closure);
561
562   DEBUGP (("no-follow in %s: %d\n", file, closure.nofollow));
563   if (meta_disallow_follow)
564     *meta_disallow_follow = closure.nofollow;
565
566   FREE_MAYBE (closure.base);
567   read_file_free (fm);
568   return closure.head;
569 }