]> sjero.net Git - wget/blob - src/recur.c
[svn] Oops! Fix braino in recur.c -- clear the hash tables only when
[wget] / src / recur.c
1 /* Handling of recursive HTTP retrieving.
2    Copyright (C) 1995, 1996, 1997, 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 #include <stdlib.h>
24 #ifdef HAVE_STRING_H
25 # include <string.h>
26 #else
27 # include <strings.h>
28 #endif /* HAVE_STRING_H */
29 #ifdef HAVE_UNISTD_H
30 # include <unistd.h>
31 #endif /* HAVE_UNISTD_H */
32 #include <errno.h>
33 #include <assert.h>
34 #include <sys/types.h>
35
36 #include "wget.h"
37 #include "url.h"
38 #include "recur.h"
39 #include "utils.h"
40 #include "retr.h"
41 #include "ftp.h"
42 #include "fnmatch.h"
43 #include "host.h"
44 #include "hash.h"
45
46 #ifndef errno
47 extern int errno;
48 #endif
49
50 extern char *version_string;
51
52 #define ROBOTS_FILENAME "robots.txt"
53
54 static struct hash_table *dl_file_url_map;
55 static struct hash_table *dl_url_file_map;
56
57 /* List of HTML files downloaded in this Wget run.  Used for link
58    conversion after Wget is done.  */
59 static slist *downloaded_html_files;
60
61 /* List of undesirable-to-load URLs.  */
62 static struct hash_table *undesirable_urls;
63
64 /* List of forbidden locations.  */
65 static char **forbidden = NULL;
66
67 /* Current recursion depth.  */
68 static int depth;
69
70 /* Base directory we're recursing from (used by no_parent).  */
71 static char *base_dir;
72
73 /* The host name for which we last checked robots.  */
74 static char *robots_host;
75
76 static int first_time = 1;
77
78 /* Construct the robots URL.  */
79 static struct urlinfo *robots_url PARAMS ((const char *, const char *));
80 static uerr_t retrieve_robots PARAMS ((const char *, const char *));
81 static char **parse_robots PARAMS ((const char *));
82 static int robots_match PARAMS ((struct urlinfo *, char **));
83
84
85 /* Cleanup the data structures associated with recursive retrieving
86    (the variables above).  */
87 void
88 recursive_cleanup (void)
89 {
90   if (undesirable_urls)
91     {
92       string_set_free (undesirable_urls);
93       undesirable_urls = NULL;
94     }
95   if (dl_file_url_map)
96     {
97       free_keys_and_values (dl_file_url_map);
98       hash_table_destroy (dl_file_url_map);
99       dl_file_url_map = NULL;
100     }
101   if (dl_url_file_map)
102     {
103       free_keys_and_values (dl_url_file_map);
104       hash_table_destroy (dl_url_file_map);
105       dl_url_file_map = NULL;
106     }
107   undesirable_urls = NULL;
108   free_vec (forbidden);
109   forbidden = NULL;
110   slist_free (downloaded_html_files);
111   downloaded_html_files = NULL;
112   FREE_MAYBE (base_dir);
113   FREE_MAYBE (robots_host);
114   first_time = 1;
115 }
116
117 /* Reset FIRST_TIME to 1, so that some action can be taken in
118    recursive_retrieve().  */
119 void
120 recursive_reset (void)
121 {
122   first_time = 1;
123 }
124
125 /* The core of recursive retrieving.  Endless recursion is avoided by
126    having all URLs stored to a linked list of URLs, which is checked
127    before loading any URL.  That way no URL can get loaded twice.
128
129    The function also supports specification of maximum recursion depth
130    and a number of other goodies.  */
131 uerr_t
132 recursive_retrieve (const char *file, const char *this_url)
133 {
134   char *constr, *filename, *newloc;
135   char *canon_this_url = NULL;
136   int dt, inl, dash_p_leaf_HTML = FALSE;
137   int meta_disallow_follow;
138   int this_url_ftp;            /* See below the explanation */
139   uerr_t err;
140   struct urlinfo *rurl;
141   urlpos *url_list, *cur_url;
142   char *rfile; /* For robots */
143   struct urlinfo *u;
144
145   assert (this_url != NULL);
146   assert (file != NULL);
147   /* If quota was exceeded earlier, bail out.  */
148   if (downloaded_exceeds_quota ())
149     return QUOTEXC;
150   /* Cache the current URL in the list.  */
151   if (first_time)
152     {
153       /* These three operations need to be done only once per Wget
154          run.  They should probably be at a different location.  */
155       if (!undesirable_urls)
156         undesirable_urls = make_string_hash_table (0);
157
158       hash_table_clear (undesirable_urls);
159       string_set_add (undesirable_urls, this_url);
160       if (dl_file_url_map)
161         hash_table_clear (dl_file_url_map);
162       if (dl_url_file_map)
163         hash_table_clear (dl_url_file_map);
164       /* Enter this_url to the hash table, in original and "enhanced" form.  */
165       u = newurl ();
166       err = parseurl (this_url, u, 0);
167       if (err == URLOK)
168         {
169           string_set_add (undesirable_urls, u->url);
170           if (opt.no_parent)
171             base_dir = xstrdup (u->dir); /* Set the base dir.  */
172           /* Set the canonical this_url to be sent as referer.  This
173              problem exists only when running the first time.  */
174           canon_this_url = xstrdup (u->url);
175         }
176       else
177         {
178           DEBUGP (("Double yuck!  The *base* URL is broken.\n"));
179           base_dir = NULL;
180         }
181       freeurl (u, 1);
182       depth = 1;
183       robots_host = NULL;
184       forbidden = NULL;
185       first_time = 0;
186     }
187   else
188     ++depth;
189
190   if (opt.reclevel != INFINITE_RECURSION && depth > opt.reclevel)
191     /* We've exceeded the maximum recursion depth specified by the user. */
192     {
193       if (opt.page_requisites && depth <= opt.reclevel + 1)
194         /* When -p is specified, we can do one more partial recursion from the
195            "leaf nodes" on the HTML document tree.  The recursion is partial in
196            that we won't traverse any <A> or <AREA> tags, nor any <LINK> tags
197            except for <LINK REL="stylesheet">. */
198         dash_p_leaf_HTML = TRUE;
199       else
200         /* Either -p wasn't specified or it was and we've already gone the one
201            extra (pseudo-)level that it affords us, so we need to bail out. */
202         {
203           DEBUGP (("Recursion depth %d exceeded max. depth %d.\n",
204                    depth, opt.reclevel));
205           --depth;
206           return RECLEVELEXC;
207         }
208     }
209
210   /* Determine whether this_url is an FTP URL.  If it is, it means
211      that the retrieval is done through proxy.  In that case, FTP
212      links will be followed by default and recursion will not be
213      turned off when following them.  */
214   this_url_ftp = (urlproto (this_url) == URLFTP);
215
216   /* Get the URL-s from an HTML file: */
217   url_list = get_urls_html (file, canon_this_url ? canon_this_url : this_url,
218                             dash_p_leaf_HTML, &meta_disallow_follow);
219
220   if (opt.use_robots && meta_disallow_follow)
221     {
222       /* The META tag says we are not to follow this file.  Respect
223          that.  */
224       free_urlpos (url_list);
225       url_list = NULL;
226     }
227
228   /* Decide what to do with each of the URLs.  A URL will be loaded if
229      it meets several requirements, discussed later.  */
230   for (cur_url = url_list; cur_url; cur_url = cur_url->next)
231     {
232       /* If quota was exceeded earlier, bail out.  */
233       if (downloaded_exceeds_quota ())
234         break;
235       /* Parse the URL for convenient use in other functions, as well
236          as to get the optimized form.  It also checks URL integrity.  */
237       u = newurl ();
238       if (parseurl (cur_url->url, u, 0) != URLOK)
239         {
240           DEBUGP (("Yuck!  A bad URL.\n"));
241           freeurl (u, 1);
242           continue;
243         }
244       if (u->proto == URLFILE)
245         {
246           DEBUGP (("Nothing to do with file:// around here.\n"));
247           freeurl (u, 1);
248           continue;
249         }
250       assert (u->url != NULL);
251       constr = xstrdup (u->url);
252
253       /* Several checkings whether a file is acceptable to load:
254          1. check if URL is ftp, and we don't load it
255          2. check for relative links (if relative_only is set)
256          3. check for domain
257          4. check for no-parent
258          5. check for excludes && includes
259          6. check for suffix
260          7. check for same host (if spanhost is unset), with possible
261          gethostbyname baggage
262          8. check for robots.txt
263
264          Addendum: If the URL is FTP, and it is to be loaded, only the
265          domain and suffix settings are "stronger".
266
267          Note that .html and (yuck) .htm will get loaded regardless of
268          suffix rules (but that is remedied later with unlink) unless
269          the depth equals the maximum depth.
270
271          More time- and memory- consuming tests should be put later on
272          the list.  */
273
274       /* inl is set if the URL we are working on (constr) is stored in
275          undesirable_urls.  Using it is crucial to avoid unnecessary
276          repeated continuous hits to the hash table.  */
277       inl = string_set_exists (undesirable_urls, constr);
278
279       /* If it is FTP, and FTP is not followed, chuck it out.  */
280       if (!inl)
281         if (u->proto == URLFTP && !opt.follow_ftp && !this_url_ftp)
282           {
283             DEBUGP (("Uh, it is FTP but i'm not in the mood to follow FTP.\n"));
284             string_set_add (undesirable_urls, constr);
285             inl = 1;
286           }
287       /* If it is absolute link and they are not followed, chuck it
288          out.  */
289       if (!inl && u->proto != URLFTP)
290         if (opt.relative_only && !cur_url->link_relative_p)
291           {
292             DEBUGP (("It doesn't really look like a relative link.\n"));
293             string_set_add (undesirable_urls, constr);
294             inl = 1;
295           }
296       /* If its domain is not to be accepted/looked-up, chuck it out.  */
297       if (!inl)
298         if (!accept_domain (u))
299           {
300             DEBUGP (("I don't like the smell of that domain.\n"));
301             string_set_add (undesirable_urls, constr);
302             inl = 1;
303           }
304       /* Check for parent directory.  */
305       if (!inl && opt.no_parent
306           /* If the new URL is FTP and the old was not, ignore
307              opt.no_parent.  */
308           && !(!this_url_ftp && u->proto == URLFTP))
309         {
310           /* Check for base_dir first.  */
311           if (!(base_dir && frontcmp (base_dir, u->dir)))
312             {
313               /* Failing that, check for parent dir.  */
314               struct urlinfo *ut = newurl ();
315               if (parseurl (this_url, ut, 0) != URLOK)
316                 DEBUGP (("Double yuck!  The *base* URL is broken.\n"));
317               else if (!frontcmp (ut->dir, u->dir))
318                 {
319                   /* Failing that too, kill the URL.  */
320                   DEBUGP (("Trying to escape parental guidance with no_parent on.\n"));
321                   string_set_add (undesirable_urls, constr);
322                   inl = 1;
323                 }
324               freeurl (ut, 1);
325             }
326         }
327       /* If the file does not match the acceptance list, or is on the
328          rejection list, chuck it out.  The same goes for the
329          directory exclude- and include- lists.  */
330       if (!inl && (opt.includes || opt.excludes))
331         {
332           if (!accdir (u->dir, ALLABS))
333             {
334               DEBUGP (("%s (%s) is excluded/not-included.\n", constr, u->dir));
335               string_set_add (undesirable_urls, constr);
336               inl = 1;
337             }
338         }
339       if (!inl)
340         {
341           char *suf = NULL;
342           /* We check for acceptance/rejection rules only for non-HTML
343              documents.  Since we don't know whether they really are
344              HTML, it will be deduced from (an OR-ed list):
345
346              1) u->file is "" (meaning it is a directory)
347              2) suffix exists, AND:
348              a) it is "html", OR
349              b) it is "htm"
350
351              If the file *is* supposed to be HTML, it will *not* be
352             subject to acc/rej rules, unless a finite maximum depth has
353             been specified and the current depth is the maximum depth. */
354           if (!
355               (!*u->file
356                || (((suf = suffix (constr)) != NULL)
357                   && ((!strcmp (suf, "html") || !strcmp (suf, "htm"))
358                       && ((opt.reclevel != INFINITE_RECURSION) &&
359                           (depth != opt.reclevel))))))
360             {
361               if (!acceptable (u->file))
362                 {
363                   DEBUGP (("%s (%s) does not match acc/rej rules.\n",
364                           constr, u->file));
365                   string_set_add (undesirable_urls, constr);
366                   inl = 1;
367                 }
368             }
369           FREE_MAYBE (suf);
370         }
371       /* Optimize the URL (which includes possible DNS lookup) only
372          after all other possibilities have been exhausted.  */
373       if (!inl)
374         {
375           if (!opt.simple_check)
376             opt_url (u);
377           else
378             {
379               char *p;
380               /* Just lowercase the hostname.  */
381               for (p = u->host; *p; p++)
382                 *p = TOLOWER (*p);
383               xfree (u->url);
384               u->url = str_url (u, 0);
385             }
386           xfree (constr);
387           constr = xstrdup (u->url);
388           string_set_add (undesirable_urls, constr);
389           if (!inl && !((u->proto == URLFTP) && !this_url_ftp))
390             if (!opt.spanhost && this_url && !same_host (this_url, constr))
391               {
392                 DEBUGP (("This is not the same hostname as the parent's.\n"));
393                 string_set_add (undesirable_urls, constr);
394                 inl = 1;
395               }
396         }
397       /* What about robots.txt?  */
398       if (!inl && opt.use_robots && u->proto == URLHTTP)
399         {
400           /* Since Wget knows about only one set of robot rules at a
401              time, /robots.txt must be reloaded whenever a new host is
402              accessed.
403
404              robots_host holds the host the current `forbid' variable
405              is assigned to.  */
406           if (!robots_host || !same_host (robots_host, u->host))
407             {
408               FREE_MAYBE (robots_host);
409               /* Now make robots_host the new host, no matter what the
410                  result will be.  So if there is no /robots.txt on the
411                  site, Wget will not retry getting robots all the
412                  time.  */
413               robots_host = xstrdup (u->host);
414               free_vec (forbidden);
415               forbidden = NULL;
416               err = retrieve_robots (constr, ROBOTS_FILENAME);
417               if (err == ROBOTSOK)
418                 {
419                   rurl = robots_url (constr, ROBOTS_FILENAME);
420                   rfile = url_filename (rurl);
421                   forbidden = parse_robots (rfile);
422                   freeurl (rurl, 1);
423                   xfree (rfile);
424                 }
425             }
426
427           /* Now that we have (or don't have) robots, we can check for
428              them.  */
429           if (!robots_match (u, forbidden))
430             {
431               DEBUGP (("Stuffing %s because %s forbids it.\n", this_url,
432                        ROBOTS_FILENAME));
433               string_set_add (undesirable_urls, constr);
434               inl = 1;
435             }
436         }
437
438       filename = NULL;
439       /* If it wasn't chucked out, do something with it.  */
440       if (!inl)
441         {
442           DEBUGP (("I've decided to load it -> "));
443           /* Add it to the list of already-loaded URL-s.  */
444           string_set_add (undesirable_urls, constr);
445           /* Automatically followed FTPs will *not* be downloaded
446              recursively.  */
447           if (u->proto == URLFTP)
448             {
449               /* Don't you adore side-effects?  */
450               opt.recursive = 0;
451             }
452           /* Reset its type.  */
453           dt = 0;
454           /* Retrieve it.  */
455           retrieve_url (constr, &filename, &newloc,
456                        canon_this_url ? canon_this_url : this_url, &dt);
457           if (u->proto == URLFTP)
458             {
459               /* Restore...  */
460               opt.recursive = 1;
461             }
462           if (newloc)
463             {
464               xfree (constr);
465               constr = newloc;
466             }
467           /* If there was no error, and the type is text/html, parse
468              it recursively.  */
469           if (dt & TEXTHTML)
470             {
471               if (dt & RETROKF)
472                 recursive_retrieve (filename, constr);
473             }
474           else
475             DEBUGP (("%s is not text/html so we don't chase.\n",
476                      filename ? filename: "(null)"));
477
478           if (opt.delete_after || (filename && !acceptable (filename)))
479             /* Either --delete-after was specified, or we loaded this otherwise
480                rejected (e.g. by -R) HTML file just so we could harvest its
481                hyperlinks -- in either case, delete the local file. */
482             {
483               DEBUGP (("Removing file due to %s in recursive_retrieve():\n",
484                        opt.delete_after ? "--delete-after" :
485                        "recursive rejection criteria"));
486               logprintf (LOG_VERBOSE,
487                          (opt.delete_after ? _("Removing %s.\n")
488                           : _("Removing %s since it should be rejected.\n")),
489                          filename);
490               if (unlink (filename))
491                 logprintf (LOG_NOTQUIET, "unlink: %s\n", strerror (errno));
492               dt &= ~RETROKF;
493             }
494
495           /* If everything was OK, and links are to be converted, let's
496              store the local filename.  */
497           if (opt.convert_links && (dt & RETROKF) && (filename != NULL))
498             {
499               cur_url->convert = CO_CONVERT_TO_RELATIVE;
500               cur_url->local_name = xstrdup (filename);
501             }
502         }
503       else
504         DEBUGP (("%s already in list, so we don't load.\n", constr));
505       /* Free filename and constr.  */
506       FREE_MAYBE (filename);
507       FREE_MAYBE (constr);
508       freeurl (u, 1);
509       /* Increment the pbuf for the appropriate size.  */
510     }
511   if (opt.convert_links && !opt.delete_after)
512     /* This is merely the first pass: the links that have been
513        successfully downloaded are converted.  In the second pass,
514        convert_all_links() will also convert those links that have NOT
515        been downloaded to their canonical form.  */
516     convert_links (file, url_list);
517   /* Free the linked list of URL-s.  */
518   free_urlpos (url_list);
519   /* Free the canonical this_url.  */
520   FREE_MAYBE (canon_this_url);
521   /* Decrement the recursion depth.  */
522   --depth;
523   if (downloaded_exceeds_quota ())
524     return QUOTEXC;
525   else
526     return RETROK;
527 }
528 \f
529 void
530 register_download (const char *url, const char *file)
531 {
532   if (!opt.convert_links)
533     return;
534   if (!dl_file_url_map)
535     dl_file_url_map = make_string_hash_table (0);
536   hash_table_put (dl_file_url_map, xstrdup (file), xstrdup (url));
537   if (!dl_url_file_map)
538     dl_url_file_map = make_string_hash_table (0);
539   hash_table_put (dl_url_file_map, xstrdup (url), xstrdup (file));
540 }
541
542 void
543 register_html (const char *url, const char *file)
544 {
545   if (!opt.convert_links)
546     return;
547   downloaded_html_files = slist_prepend (downloaded_html_files, file);
548 }
549
550 /* convert_links() is called from recursive_retrieve() after we're
551    done with an HTML file.  This call to convert_links is not complete
552    because it converts only the downloaded files, and Wget cannot know
553    which files will be downloaded afterwards.  So, if we have file
554    fileone.html with:
555
556    <a href="/c/something.gif">
557
558    and /c/something.gif was not downloaded because it exceeded the
559    recursion depth, the reference will *not* be changed.
560
561    However, later we can encounter /c/something.gif from an "upper"
562    level HTML (let's call it filetwo.html), and it gets downloaded.
563
564    But now we have a problem because /c/something.gif will be
565    correctly transformed in filetwo.html, but not in fileone.html,
566    since Wget could not have known that /c/something.gif will be
567    downloaded in the future.
568
569    This is why Wget must, after the whole retrieval, call
570    convert_all_links to go once more through the entire list of
571    retrieved HTMLs, and re-convert them.
572
573    All the downloaded HTMLs are kept in downloaded_html_files, and downloaded URLs
574    in urls_downloaded.  From these two lists information is
575    extracted.  */
576 void
577 convert_all_links (void)
578 {
579   slist *html;
580
581   /* Destructively reverse downloaded_html_files to get it in the right order.
582      recursive_retrieve() used slist_prepend() consistently.  */
583   downloaded_html_files = slist_nreverse (downloaded_html_files);
584
585   for (html = downloaded_html_files; html; html = html->next)
586     {
587       urlpos *urls, *cur_url;
588       char *url;
589
590       DEBUGP (("Rescanning %s\n", html->string));
591       /* Determine the URL of the HTML file.  get_urls_html will need
592          it.  */
593       url = hash_table_get (dl_file_url_map, html->string);
594       if (url)
595         DEBUGP (("It should correspond to %s.\n", url));
596       else
597         DEBUGP (("I cannot find the corresponding URL.\n"));
598       /* Parse the HTML file...  */
599       urls = get_urls_html (html->string, url, FALSE, NULL);
600       /* We don't respect meta_disallow_follow here because, even if
601          the file is not followed, we might still want to convert the
602          links that have been followed from other files.  */
603       for (cur_url = urls; cur_url; cur_url = cur_url->next)
604         {
605           char *local_name;
606
607           /* The URL must be in canonical form to be compared.  */
608           struct urlinfo *u = newurl ();
609           uerr_t res = parseurl (cur_url->url, u, 0);
610           if (res != URLOK)
611             {
612               freeurl (u, 1);
613               continue;
614             }
615           /* We decide the direction of conversion according to whether
616              a URL was downloaded.  Downloaded URLs will be converted
617              ABS2REL, whereas non-downloaded will be converted REL2ABS.  */
618           local_name = hash_table_get (dl_url_file_map, u->url);
619           if (local_name)
620             DEBUGP (("%s marked for conversion, local %s\n",
621                      u->url, local_name));
622           /* Decide on the conversion direction.  */
623           if (local_name)
624             {
625               /* We've downloaded this URL.  Convert it to relative
626                  form.  We do this even if the URL already is in
627                  relative form, because our directory structure may
628                  not be identical to that on the server (think `-nd',
629                  `--cut-dirs', etc.)  */
630               cur_url->convert = CO_CONVERT_TO_RELATIVE;
631               cur_url->local_name = xstrdup (local_name);
632             }
633           else
634             {
635               /* We haven't downloaded this URL.  If it's not already
636                  complete (including a full host name), convert it to
637                  that form, so it can be reached while browsing this
638                  HTML locally.  */
639               if (!cur_url->link_complete_p)
640                 cur_url->convert = CO_CONVERT_TO_COMPLETE;
641               cur_url->local_name = NULL;
642             }
643           freeurl (u, 1);
644         }
645       /* Convert the links in the file.  */
646       convert_links (html->string, urls);
647       /* Free the data.  */
648       free_urlpos (urls);
649     }
650 }
651 \f
652 /* Robots support.  */
653
654 /* Construct the robots URL.  */
655 static struct urlinfo *
656 robots_url (const char *url, const char *robots_filename)
657 {
658   struct urlinfo *u = newurl ();
659   uerr_t err;
660
661   err = parseurl (url, u, 0);
662   assert (err == URLOK && u->proto == URLHTTP);
663   xfree (u->file);
664   xfree (u->dir);
665   xfree (u->url);
666   u->dir = xstrdup ("");
667   u->file = xstrdup (robots_filename);
668   u->url = str_url (u, 0);
669   return u;
670 }
671
672 /* Retrieves the robots_filename from the root server directory, if
673    possible.  Returns ROBOTSOK if robots were retrieved OK, and
674    NOROBOTS if robots could not be retrieved for any reason.  */
675 static uerr_t
676 retrieve_robots (const char *url, const char *robots_filename)
677 {
678   int dt;
679   uerr_t err;
680   struct urlinfo *u;
681
682   u = robots_url (url, robots_filename);
683   logputs (LOG_VERBOSE, _("Loading robots.txt; please ignore errors.\n"));
684   err = retrieve_url (u->url, NULL, NULL, NULL, &dt);
685   freeurl (u, 1);
686   if (err == RETROK)
687     return ROBOTSOK;
688   else
689     return NOROBOTS;
690 }
691
692 /* Parse the robots_filename and return the disallowed path components
693    in a malloc-ed vector of character pointers.
694
695    It should be fully compliant with the syntax as described in the
696    file norobots.txt, adopted by the robots mailing list
697    (robots@webcrawler.com).  */
698 static char **
699 parse_robots (const char *robots_filename)
700 {
701   FILE *fp;
702   char **entries;
703   char *line, *cmd, *str, *p;
704   char *base_version, *version;
705   int num, i;
706   int wget_matched;             /* is the part meant for Wget?  */
707
708   entries = NULL;
709
710   num = 0;
711   fp = fopen (robots_filename, "rb");
712   if (!fp)
713     return NULL;
714
715   /* Kill version number.  */
716   if (opt.useragent)
717     {
718       STRDUP_ALLOCA (base_version, opt.useragent);
719       STRDUP_ALLOCA (version, opt.useragent);
720     }
721   else
722     {
723       int len = 10 + strlen (version_string);
724       base_version = (char *)alloca (len);
725       sprintf (base_version, "Wget/%s", version_string);
726       version = (char *)alloca (len);
727       sprintf (version, "Wget/%s", version_string);
728     }
729   for (p = version; *p; p++)
730     *p = TOLOWER (*p);
731   for (p = base_version; *p && *p != '/'; p++)
732     *p = TOLOWER (*p);
733   *p = '\0';
734
735   /* Setting this to 1 means that Wget considers itself under
736      restrictions by default, even if the User-Agent field is not
737      present.  However, if it finds the user-agent set to anything
738      other than Wget, the rest will be ignored (up to the following
739      User-Agent field).  Thus you may have something like:
740
741      Disallow: 1
742      Disallow: 2
743      User-Agent: stupid-robot
744      Disallow: 3
745      Disallow: 4
746      User-Agent: Wget*
747      Disallow: 5
748      Disallow: 6
749      User-Agent: *
750      Disallow: 7
751
752      In this case the 1, 2, 5, 6 and 7 disallow lines will be
753      stored.  */
754   wget_matched = 1;
755   while ((line = read_whole_line (fp)))
756     {
757       int len = strlen (line);
758       /* Destroy <CR><LF> if present.  */
759       if (len && line[len - 1] == '\n')
760         line[--len] = '\0';
761       if (len && line[len - 1] == '\r')
762         line[--len] = '\0';
763       /* According to specifications, optional space may be at the
764          end...  */
765       DEBUGP (("Line: %s\n", line));
766       /* Skip spaces.  */
767       for (cmd = line; *cmd && ISSPACE (*cmd); cmd++);
768       if (!*cmd)
769         {
770           xfree (line);
771           DEBUGP (("(chucked out)\n"));
772           continue;
773         }
774       /* Look for ':'.  */
775       for (str = cmd; *str && *str != ':'; str++);
776       if (!*str)
777         {
778           xfree (line);
779           DEBUGP (("(chucked out)\n"));
780           continue;
781         }
782       /* Zero-terminate the command.  */
783       *str++ = '\0';
784       /* Look for the string beginning...  */
785       for (; *str && ISSPACE (*str); str++);
786       /* Look for comments or trailing spaces and kill them off.  */
787       for (p = str; *p; p++)
788         if (*p && ISSPACE (*p) && ((*(p + 1) == '#') || (*(p + 1) == '\0')))
789           {
790             /* We have found either a shell-style comment `<sp>+#' or some
791                trailing spaces.  Now rewind to the beginning of the spaces
792                and place '\0' there.  */
793             while (p > str && ISSPACE (*p))
794               --p;
795             if (p == str)
796               *p = '\0';
797             else
798               *(p + 1) = '\0';
799             break;
800           }
801       if (!strcasecmp (cmd, "User-agent"))
802         {
803           int match = 0;
804           /* Lowercase the agent string.  */
805           for (p = str; *p; p++)
806             *p = TOLOWER (*p);
807           /* If the string is `*', it matches.  */
808           if (*str == '*' && !*(str + 1))
809             match = 1;
810           else
811             {
812               /* If the string contains wildcards, we'll run it through
813                  fnmatch().  */
814               if (has_wildcards_p (str))
815                 {
816                   /* If the string contains '/', compare with the full
817                      version.  Else, compare it to base_version.  */
818                   if (strchr (str, '/'))
819                     match = !fnmatch (str, version, 0);
820                   else
821                     match = !fnmatch (str, base_version, 0);
822                 }
823               else                /* Substring search */
824                 {
825                   if (strstr (version, str))
826                     match = 1;
827                   else
828                     match = 0;
829                 }
830             }
831           /* If Wget is not matched, skip all the entries up to the
832              next User-agent field.  */
833           wget_matched = match;
834         }
835       else if (!wget_matched)
836         {
837           xfree (line);
838           DEBUGP (("(chucking out since it is not applicable for Wget)\n"));
839           continue;
840         }
841       else if (!strcasecmp (cmd, "Disallow"))
842         {
843           /* If "Disallow" is empty, the robot is welcome.  */
844           if (!*str)
845             {
846               free_vec (entries);
847               entries = (char **)xmalloc (sizeof (char *));
848               *entries = NULL;
849               num = 0;
850             }
851           else
852             {
853               entries = (char **)xrealloc (entries, (num + 2)* sizeof (char *));
854               entries[num] = xstrdup (str);
855               entries[++num] = NULL;
856               /* Strip trailing spaces, according to specifications.  */
857               for (i = strlen (str); i >= 0 && ISSPACE (str[i]); i--)
858                 if (ISSPACE (str[i]))
859                   str[i] = '\0';
860             }
861         }
862       else
863         {
864           /* unknown command */
865           DEBUGP (("(chucked out)\n"));
866         }
867       xfree (line);
868     }
869   fclose (fp);
870   return entries;
871 }
872
873 /* May the URL url be loaded according to disallowing rules stored in
874    forbidden?  */
875 static int
876 robots_match (struct urlinfo *u, char **fb)
877 {
878   int l;
879
880   if (!fb)
881     return 1;
882   DEBUGP (("Matching %s against: ", u->path));
883   for (; *fb; fb++)
884     {
885       DEBUGP (("%s ", *fb));
886       l = strlen (*fb);
887       /* If dir is fb, we may not load the file.  */
888       if (strncmp (u->path, *fb, l) == 0)
889         {
890           DEBUGP (("matched.\n"));
891           return 0; /* Matches, i.e. does not load...  */
892         }
893     }
894   DEBUGP (("not matched.\n"));
895   return 1;
896 }