c913cef45803db5a80edc979747420c7bb187623
[xscreensaver] / hacks / webcollage
1 #!/usr/bin/perl -w
2 #
3 # webcollage, Copyright (c) 1999-2003 by Jamie Zawinski <jwz@jwz.org>
4 # This program decorates the screen with random images from the web.
5 # One satisfied customer described it as "a nonstop pop culture brainbath."
6 #
7 # Permission to use, copy, modify, distribute, and sell this software and its
8 # documentation for any purpose is hereby granted without fee, provided that
9 # the above copyright notice appear in all copies and that both that
10 # copyright notice and this permission notice appear in supporting
11 # documentation.  No representations are made about the suitability of this
12 # software for any purpose.  It is provided "as is" without express or
13 # implied warranty.
14
15
16 # To run this as a display mode with xscreensaver, add this to `programs':
17 #
18 #   default-n:  webcollage -root                                        \n\
19 #   default-n:  webcollage -root -filter 'vidwhacker -stdin -stdout'    \n\
20
21
22 # If you have the "driftnet" program installed, webcollage can display a
23 # collage of images sniffed off your local ethernet, instead of pulled out
24 # of search engines: in that way, your screensaver can display the images
25 # that your co-workers are downloading!
26 #
27 # Driftnet is available here: http://www.ex-parrot.com/~chris/driftnet/
28 # Use it like so:
29 #
30 #   default-n:  webcollage -root -driftnet                             \n\
31 #
32 # Driftnet is the Unix implementation of the MacOS "EtherPEG" program.
33
34
35 require 5;
36 use strict;
37
38 # We can't "use diagnostics" here, because that library malfunctions if
39 # you signal and catch alarms: it says "Uncaught exception from user code"
40 # and exits, even though I damned well AM catching it!
41 #use diagnostics;
42
43
44 use Socket;
45 require Time::Local;
46 require POSIX;
47 use Fcntl ':flock'; # import LOCK_* constants
48 use POSIX qw(strftime);
49
50 use bytes;  # Larry can take Unicode and shove it up his ass sideways.
51             # Perl 5.8.0 causes us to start getting incomprehensible
52             # errors about UTF-8 all over the place without this.
53
54
55 my $progname = $0; $progname =~ s@.*/@@g;
56 my $version = q{ $Revision: 1.103 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
57 my $copyright = "WebCollage $version, Copyright (c) 1999-2002" .
58     " Jamie Zawinski <jwz\@jwz.org>\n" .
59     "            http://www.jwz.org/xscreensaver/\n";
60
61
62
63 my @search_methods = (  77, "altavista",  \&pick_from_alta_vista_random_link,
64                         14, "yahoorand",  \&pick_from_yahoo_random_link,
65                          9, "yahoonews",  \&pick_from_yahoo_news_text,
66
67                      # Alta Vista has a new "random link" URL now.
68                      # They added it specifically to better support webcollage!
69                      # That was super cool of them.  This is how we used to do
70                      # it, before:
71                      #
72                      #  0, "avimages", \&pick_from_alta_vista_images,
73                      #  0, "avtext",   \&pick_from_alta_vista_text,
74
75                      # Google asked (nicely) for me to stop searching them.
76                      # I asked them to add a "random link" url.  They said
77                      # "that would be easy, we'll think about it" and then
78                      # never wrote back.  Booo Google!  Booooo!
79                      #
80                      #   0, "googlenums", \&pick_from_google_image_numbers,
81                      #   0, "googleimgs", \&pick_from_google_images,
82
83                      # I suspect Hotbot is actually the same search engine
84                      # data as Lycos.
85                      #
86                      #  0, "hotbot",     \&pick_from_hotbot_text,
87
88                      # Eh, Lycos sucks anyway.
89                      #   0, "lycos",      \&pick_from_lycos_text,
90                       );
91
92 # programs we can use to write to the root window (tried in ascending order.)
93 #
94 my @root_displayers = (
95   "chbg       -once -xscreensaver -max_size 100",
96   "xv         -root -quit -viewonly +noresetroot -quick24 -rmode 5" .
97   "           -rfg black -rbg black",
98   "xli        -quiet -onroot -center -border black",
99   "xloadimage -quiet -onroot -center -border black",
100
101 # this lame program wasn't built with vroot.h:
102 # "xsri       -scale -keep-aspect -center-horizontal -center-vertical",
103 );
104
105
106 # Some sites need cookies to work properly.   These are they.
107 #
108 my %cookies = (
109   "www.altavista.com"  =>  "AV_ALL=1",   # request uncensored searches
110   "web.altavista.com"  =>  "AV_ALL=1",
111
112                                          # log in as "cipherpunk"
113   "www.nytimes.com"    =>  'NYT-S=18cHMIlJOn2Y1bu5xvEG3Ufuk6E1oJ.' .
114                            'FMxWaQV0igaB5Yi/Q/guDnLeoL.pe7i1oakSb' .
115                            '/VqfdUdb2Uo27Vzt1jmPn3cpYRlTw9',
116 );
117
118
119 # If this is set, it's a helper program to use for pasting images together:
120 # this is a lot faster and more efficient than using PPM pipelines, which is
121 # what we do if this program doesn't exist.  (We check for "webcollage-helper"
122 # on $PATH at startup, and set this variable appropriately.)
123 #
124 my $webcollage_helper = undef;
125
126
127 # If we have the webcollage-helper program, then it will paste the images
128 # together with transparency!  0.0 is invisible, 1.0 is totally opaque.
129 #
130 my $opacity = 0.85;
131
132
133 # Some sites have  managed to poison the search engines.  These are they.
134 # (We auto-detect sites that have poisoned the search engines via excessive
135 # keywords or dictionary words,  but these are ones that slip through
136 # anyway.)
137 #
138 # This can contain full host names, or 2 or 3 component domains.
139 #
140 my %poisoners = (
141   "die.net"                 => 1,  # 'l33t h4ck3r d00dz.
142   "genforum.genealogy.com"  => 1,  # Cluttering avtext with human names.
143   "rootsweb.com"            => 1,  # Cluttering avtext with human names.
144   "akamai.net"              => 1,  # Lots of sites have their images on Akamai.
145   "akamaitech.net"          => 1,  # But those are pretty much all banners.
146                                    # Since Akamai is super-expensive, let's
147                                    # go out on a limb and assume that all of
148                                    # their customers are rich-and-boring.
149   "bartleby.com"            => 1,  # Dictionary, cluttering avtext.
150   "encyclopedia.com"        => 1,  # Dictionary, cluttering avtext.
151   "onlinedictionary.datasegment.com" => 1,  # Dictionary, cluttering avtext.
152   "hotlinkpics.com"         => 1,  # Porn site that has poisoned avimages
153                                    # (I don't see how they did it, though!)
154   "alwayshotels.com"        => 1,  # Poisoned Lycos pretty heavily.
155   "nextag.com"              => 1,  # Poisoned Alta Vista real good.
156 );
157
158
159 # When verbosity is turned on, we warn about sites that we seem to be hitting
160 # a lot: usually this means some new poisoner has made it into the search
161 # engines.  But sometimes, the warning is just because that site has a lot
162 # of stuff on it.  So these are the sites that are immune to the "frequent
163 # site" diagnostic message.
164 #
165 my %warningless_sites = (
166   "home.earthlink.net"      => 1,  # Lots of home pages here.
167   "www.geocities.com"       => 1,
168   "www.angelfire.com"       => 1,
169   "members.aol.com"         => 1,
170
171   "yimg.com"                => 1,  # This is where dailynews.yahoo.com stores
172   "eimg.com"                => 1,  # its images, so pick_from_yahoo_news_text()
173                                    # hits this every time.
174
175   "driftnet"                => 1,  # builtin...
176 );
177
178
179 ##############################################################################
180 #
181 # Various global flags set by command line parameters, or computed
182 #
183 ##############################################################################
184
185
186 my $current_state = "???";      # for diagnostics
187 my $load_method;
188 my $last_search;
189 my $image_succeeded = -1;
190 my $suppress_audit = 0;
191
192 my $verbose_imgmap = 0;         # print out rectangles and URLs only (stdout)
193 my $verbose_warnings = 0;       # print out warnings when things go wrong
194 my $verbose_load = 0;           # diagnostics about loading of URLs
195 my $verbose_filter = 0;         # diagnostics about page selection/rejection
196 my $verbose_net = 0;            # diagnostics about network I/O
197 my $verbose_pbm = 0;            # diagnostics about PBM pipelines
198 my $verbose_http = 0;           # diagnostics about all HTTP activity
199 my $verbose_exec = 0;           # diagnostics about executing programs
200
201 my $report_performance_interval = 60 * 15;  # print some stats every 15 minutes
202
203 my $http_proxy = undef;
204 my $http_timeout = 30;
205 my $cvt_timeout = 10;
206
207 my $min_width = 50;
208 my $min_height = 50;
209 my $min_ratio = 1/5;
210
211 my $min_gif_area = (120 * 120);
212
213
214 my $no_output_p = 0;
215 my $urls_only_p = 0;
216
217 my @pids_to_kill = ();  # forked pids we should kill when we exit, if any.
218
219 my $driftnet_magic = 'driftnet';
220 my $driftnet_dir = undef;
221 my $default_driftnet_cmd = "driftnet -a -m 100";
222
223 my $wordlist;
224
225 my %rejected_urls;
226 my @tripwire_words = ("aberrate", "abode", "amorphous", "antioch",
227                       "arrhenius", "arteriole", "blanket", "brainchild",
228                       "burdensome", "carnival", "cherub", "chord", "clever",
229                       "dedicate", "dilogarithm", "dolan", "dryden",
230                       "eggplant");
231
232
233 ##############################################################################
234 #
235 # Retrieving URLs
236 #
237 ##############################################################################
238
239 # returns three values: the HTTP response line; the document headers;
240 # and the document body.
241 #
242 sub get_document_1 {
243   my ( $url, $referer, $timeout ) = @_;
244
245   if (!defined($timeout)) { $timeout = $http_timeout; }
246   if ($timeout > $http_timeout) { $timeout = $http_timeout; }
247
248   if ($timeout <= 0) {
249     LOG (($verbose_net || $verbose_load), "timed out for $url");
250     return ();
251   }
252
253   LOG ($verbose_net, "get_document_1 $url " . ($referer ? $referer : ""));
254
255   if (! ($url =~ m@^http://@i)) {
256     LOG ($verbose_net, "not an HTTP URL: $url");
257     return ();
258   }
259
260   my ($url_proto, $dummy, $serverstring, $path) = split(/\//, $url, 4);
261   $path = "" unless $path;
262
263   my ($them,$port) = split(/:/, $serverstring);
264   $port = 80 unless $port;
265
266   my $them2 = $them;
267   my $port2 = $port;
268   if ($http_proxy) {
269     $serverstring = $http_proxy if $http_proxy;
270     ($them2,$port2) = split(/:/, $serverstring);
271     $port2 = 80 unless $port2;
272   }
273
274   my ($remote, $iaddr, $paddr, $proto, $line);
275   $remote = $them2;
276   if ($port2 =~ /\D/) { $port2 = getservbyname($port2, 'tcp') }
277   if (!$port2) {
278     LOG (($verbose_net || $verbose_load), "unrecognised port in $url");
279     return ();
280   }
281   $iaddr   = inet_aton($remote);
282   if (!$iaddr) {
283     LOG (($verbose_net || $verbose_load), "host not found: $remote");
284     return ();
285   }
286   $paddr   = sockaddr_in($port2, $iaddr);
287
288
289   my $head = "";
290   my $body = "";
291
292   @_ =
293     eval {
294       local $SIG{ALRM} = sub {
295         LOG (($verbose_net || $verbose_load), "timed out ($timeout) for $url");
296         die "alarm\n";
297       };
298       alarm $timeout;
299
300       $proto   = getprotobyname('tcp');
301       if (!socket(S, PF_INET, SOCK_STREAM, $proto)) {
302         LOG (($verbose_net || $verbose_load), "socket: $!");
303         return ();
304       }
305       if (!connect(S, $paddr)) {
306         LOG (($verbose_net || $verbose_load), "connect($serverstring): $!");
307         return ();
308       }
309
310       select(S); $| = 1; select(STDOUT);
311
312       my $cookie = $cookies{$them};
313
314       my $user_agent = "$progname/$version";
315
316       if ($url =~ m@^http://www\.altavista\.com/@ ||
317           $url =~ m@^http://random\.yahoo\.com/@) {
318         # block this, you turkeys.
319         $user_agent = "Mozilla/4.76 [en] (X11; U; Linux 2.2.16-22 i686; Nav)";
320       }
321
322       my $hdrs = "GET " . ($http_proxy ? $url : "/$path") . " HTTP/1.0\r\n" .
323                  "Host: $them\r\n" .
324                  "User-Agent: $user_agent\r\n";
325       if ($referer) {
326         $hdrs .= "Referer: $referer\r\n";
327       }
328       if ($cookie) {
329         my @cc = split(/\r?\n/, $cookie);
330         $hdrs .= "Cookie: " . join('; ', @cc) . "\r\n";
331       }
332       $hdrs .= "\r\n";
333
334       foreach (split('\r?\n', $hdrs)) {
335         LOG ($verbose_http, "  ==> $_");
336       }
337       print S $hdrs;
338       my $http = <S> || "";
339
340       $_  = $http;
341       s/[\r\n]+$//s;
342       LOG ($verbose_http, "  <== $_");
343
344       while (<S>) {
345         $head .= $_;
346         s/[\r\n]+$//s;
347         last if m@^$@;
348         LOG ($verbose_http, "  <== $_");
349
350         if (m@^Set-cookie:\s*([^;\r\n]+)@i) {
351           set_cookie($them, $1)
352         }
353       }
354
355       my $lines = 0;
356       while (<S>) {
357         $body .= $_;
358         $lines++;
359       }
360
361       LOG ($verbose_http,
362            "  <== [ body ]: $lines lines, " . length($body) . " bytes");
363
364       close S;
365
366       if (!$http) {
367         LOG (($verbose_net || $verbose_load), "null response: $url");
368         return ();
369       }
370
371       return ( $http, $head, $body );
372     };
373   die if ($@ && $@ ne "alarm\n");       # propagate errors
374   if ($@) {
375     # timed out
376     $head = undef;
377     $body = undef;
378     $suppress_audit = 1;
379     return ();
380   } else {
381     # didn't
382     alarm 0;
383     return @_;
384   }
385 }
386
387
388 # returns two values: the document headers; and the document body.
389 # if the given URL did a redirect, returns the redirected-to document.
390 #
391 sub get_document {
392   my ( $url, $referer, $timeout ) = @_;
393   my $start = time;
394
395   if (defined($referer) && $referer eq $driftnet_magic) {
396     return get_driftnet_file ($url);
397   }
398
399   my $orig_url = $url;
400   my $loop_count = 0;
401   my $max_loop_count = 4;
402
403   do {
404     if (defined($timeout) && $timeout <= 0) {
405       LOG (($verbose_net || $verbose_load), "timed out for $url");
406       $suppress_audit = 1;
407       return ();
408     }
409
410     my ( $http, $head, $body ) = get_document_1 ($url, $referer, $timeout);
411
412     if (defined ($timeout)) {
413       my $now = time;
414       my $elapsed = $now - $start;
415       $timeout -= $elapsed;
416       $start = $now;
417     }
418
419     return () unless $http; # error message already printed
420
421     $http =~ s/[\r\n]+$//s;
422
423     if ( $http =~ m@^HTTP/[0-9.]+ 30[123]@ ) {
424       $_ = $head;
425       my ( $location ) = m@^location:[ \t]*(.*)$@im;
426       if ( $location ) {
427         $location =~ s/[\r\n]$//;
428
429         LOG ($verbose_net, "redirect from $url to $location");
430         $referer = $url;
431         $url = $location;
432
433         if ($url =~ m@^/@) {
434           $referer =~ m@^(http://[^/]+)@i;
435           $url = $1 . $url;
436         } elsif (! ($url =~ m@^[a-z]+:@i)) {
437           $_ = $referer;
438           s@[^/]+$@@g if m@^http://[^/]+/@i;
439           $_ .= "/" if m@^http://[^/]+$@i;
440           $url = $_ . $url;
441         }
442
443       } else {
444         LOG ($verbose_net, "no Location with \"$http\"");
445         return ( $url, $body );
446       }
447
448       if ($loop_count++ > $max_loop_count) {
449         LOG ($verbose_net,
450              "too many redirects ($max_loop_count) from $orig_url");
451         $body = undef;
452         return ();
453       }
454
455     } elsif ( $http =~ m@^HTTP/[0-9.]+ ([4-9][0-9][0-9].*)$@ ) {
456
457       LOG (($verbose_net || $verbose_load), "failed: $1 ($url)");
458
459       # http errors -- return nothing.
460       $body = undef;
461       return ();
462
463     } elsif (!$body) {
464
465       LOG (($verbose_net || $verbose_load), "document contains no data: $url");
466       return ();
467
468     } else {
469
470       # ok!
471       return ( $url, $body );
472     }
473
474   } while (1);
475 }
476
477 # If we already have a cookie defined for this site, and the site is trying
478 # to overwrite that very same cookie, let it do so.  This is because nytimes
479 # expires its cookies - it lets you upgrade to a new cookie without logging
480 # in again, but you have to present the old cookie to get the new cookie.
481 # So, by doing this, the built-in cypherpunks cookie will never go "stale".
482 #
483 sub set_cookie {
484   my ($host, $cookie) = @_;
485   my $oc = $cookies{$host};
486   return unless $oc;
487   $_ = $oc;
488   my ($oc_name, $oc_value) = m@^([^= \t\r\n]+)=(.*)$@;
489   $_ = $cookie;
490   my ($nc_name, $nc_value) = m@^([^= \t\r\n]+)=(.*)$@;
491
492   if ($oc_name eq $nc_name &&
493       $oc_value ne $nc_value) {
494     $cookies{$host} = $cookie;
495     LOG ($verbose_net, "overwrote ${host}'s $oc_name cookie");
496   }
497 }
498
499
500 ############################################################################
501 #
502 # Extracting image URLs from HTML
503 #
504 ############################################################################
505
506 # given a URL and the body text at that URL, selects and returns a random
507 # image from it.  returns () if no suitable images found.
508 #
509 sub pick_image_from_body {
510   my ( $url, $body ) = @_;
511
512   my $base = $url;
513   $_ = $url;
514
515   # if there's at least one slash after the host, take off the last
516   # pathname component
517   if ( m@^http://[^/]+/@io ) {
518     $base =~ s@[^/]+$@@go;
519   }
520
521   # if there are no slashes after the host at all, put one on the end.
522   if ( m@^http://[^/]+$@io ) {
523     $base .= "/";
524   }
525
526   $_ = $body;
527
528   # strip out newlines, compress whitespace
529   s/[\r\n\t ]+/ /go;
530
531   # nuke comments
532   s/<!--.*?-->//go;
533
534
535   # There are certain web sites that list huge numbers of dictionary
536   # words in their bodies or in their <META NAME=KEYWORDS> tags (surprise!
537   # Porn sites tend not to be reputable!)
538   #
539   # I do not want webcollage to filter on content: I want it to select
540   # randomly from the set of images on the web.  All the logic here for
541   # rejecting some images is really a set of heuristics for rejecting
542   # images that are not really images: for rejecting *text* that is in
543   # GIF/JPEG form.  I don't want text, I want pictures, and I want the
544   # content of the pictures to be randomly selected from among all the
545   # available content.
546   #
547   # So, filtering out "dirty" pictures by looking for "dirty" keywords
548   # would be wrong: dirty pictures exist, like it or not, so webcollage
549   # should be able to select them.
550   #
551   # However, picking a random URL is a hard thing to do.  The mechanism I'm
552   # using is to search for a selection of random words.  This is not
553   # perfect, but works ok most of the time.  The way it breaks down is when
554   # some URLs get precedence because their pages list *every word* as
555   # related -- those URLs come up more often than others.
556   #
557   # So, after we've retrieved a URL, if it has too many keywords, reject
558   # it.  We reject it not on the basis of what those keywords are, but on
559   # the basis that by having so many, the page has gotten an unfair
560   # advantage against our randomizer.
561   #
562   my $trip_count = 0;
563   foreach my $trip (@tripwire_words) {
564     $trip_count++ if m/$trip/i;
565   }
566
567   if ($trip_count >= $#tripwire_words - 2) {
568     LOG (($verbose_filter || $verbose_load),
569          "there is probably a dictionary in \"$url\": rejecting.");
570     $rejected_urls{$url} = -1;
571     $body = undef;
572     $_ = undef;
573     return ();
574   }
575
576
577   my @urls;
578   my %unique_urls;
579
580   foreach (split(/ *</)) {
581     if ( m/^meta /i ) {
582
583       # Likewise, reject any web pages that have a KEYWORDS meta tag
584       # that is too long.
585       #
586       if (m/name ?= ?\"?keywords\"?/i &&
587           m/content ?= ?\"([^\"]+)\"/) {
588         my $L = length($1);
589         if ($L > 1000) {
590           LOG (($verbose_filter || $verbose_load),
591                "excessive keywords ($L bytes) in $url: rejecting.");
592           $rejected_urls{$url} = $L;
593           $body = undef;
594           $_ = undef;
595           return ();
596         } else {
597           LOG ($verbose_filter, "  keywords ($L bytes) in $url (ok)");
598         }
599       }
600
601     } elsif ( m/^(img|a) .*(src|href) ?= ?\"? ?(.*?)[ >\"]/io ) {
602
603       my $was_inline = (! ( "$1" eq "a" || "$1" eq "A" ));
604       my $link = $3;
605       my ( $width )  = m/width ?=[ \"]*(\d+)/oi;
606       my ( $height ) = m/height ?=[ \"]*(\d+)/oi;
607       $_ = $link;
608
609       if ( m@^/@o ) {
610         my $site;
611         ( $site = $base ) =~ s@^(http://[^/]*).*@$1@gio;
612         $_ = "$site$link";
613       } elsif ( ! m@^[^/:?]+:@ ) {
614         $_ = "$base$link";
615         s@/\./@/@g;
616         1 while (s@/[^/]+/\.\./@/@g);
617       }
618
619       # skip non-http
620       if ( ! m@^http://@io ) {
621         next;
622       }
623
624       # skip non-image
625       if ( ! m@[.](gif|jpg|jpeg|pjpg|pjpeg)$@io ) {
626         next;
627       }
628
629       # skip really short or really narrow images
630       if ( $width && $width < $min_width) {
631         if (!$height) { $height = "?"; }
632         LOG ($verbose_filter, "  skip narrow image $_ (${width}x$height)");
633         next;
634       }
635
636       if ( $height && $height < $min_height) {
637         if (!$width) { $width = "?"; }
638         LOG ($verbose_filter, "  skip short image $_ (${width}x$height)");
639         next;
640       }
641
642       # skip images with ratios that make them look like banners.
643       if ($min_ratio && $width && $height &&
644           ($width * $min_ratio ) > $height) {
645         if (!$height) { $height = "?"; }
646         LOG ($verbose_filter, "  skip bad ratio $_ (${width}x$height)");
647         next;
648       }
649
650       # skip GIFs with a small number of pixels -- those usually suck.
651       if ($width && $height &&
652           m/\.gif$/io &&
653           ($width * $height) < $min_gif_area) {
654         LOG ($verbose_filter, "  skip small GIF $_ (${width}x$height)");
655         next;
656       }
657       
658
659       my $url = $_;
660
661       if ($unique_urls{$url}) {
662         LOG ($verbose_filter, "  skip duplicate image $_");
663         next;
664       }
665
666       LOG ($verbose_filter,
667            "  image $url" .
668            ($width && $height ? " (${width}x${height})" : "") .
669            ($was_inline ? " (inline)" : ""));
670
671       $urls[++$#urls] = $url;
672       $unique_urls{$url}++;
673
674       # jpegs are preferable to gifs.
675       $_ = $url;
676       if ( ! m@[.]gif$@io ) {
677         $urls[++$#urls] = $url;
678       }
679
680       # pointers to images are preferable to inlined images.
681       if ( ! $was_inline ) {
682         $urls[++$#urls] = $url;
683         $urls[++$#urls] = $url;
684       }
685     }
686   }
687
688   my $fsp = ($body =~ m@<frameset@i);
689
690   $_ = undef;
691   $body = undef;
692
693   @urls = depoison (@urls);
694
695   if ( $#urls < 0 ) {
696     LOG ($verbose_load, "no images on $base" . ($fsp ? " (frameset)" : ""));
697     return ();
698   }
699
700   # pick a random element of the table
701   my $i = int(rand($#urls+1));
702   $url = $urls[$i];
703
704   LOG ($verbose_load, "picked image " .($i+1) . "/" . ($#urls+1) . ": $url");
705
706   return $url;
707 }
708
709
710 \f
711 ############################################################################
712 #
713 # Subroutines for getting pages and images out of search engines
714 #
715 ############################################################################
716
717
718 sub pick_dictionary {
719   my @dicts = ("/usr/dict/words",
720                "/usr/share/dict/words",
721                "/usr/share/lib/dict/words");
722   foreach my $f (@dicts) {
723     if (-f $f) {
724       $wordlist = $f;
725       last;
726     }
727   }
728   error ("$dicts[0] does not exist") unless defined($wordlist);
729 }
730
731 # returns a random word from the dictionary
732 #
733 sub random_word {
734     my $word = 0;
735     if (open (IN, "<$wordlist")) {
736         my $size = (stat(IN))[7];
737         my $pos = rand $size;
738         if (seek (IN, $pos, 0)) {
739             $word = <IN>;   # toss partial line
740             $word = <IN>;   # keep next line
741         }
742         if (!$word) {
743           seek( IN, 0, 0 );
744           $word = <IN>;
745         }
746         close (IN);
747     }
748
749     return 0 if (!$word);
750
751     $word =~ s/^[ \t\n\r]+//;
752     $word =~ s/[ \t\n\r]+$//;
753     $word =~ s/ys$/y/;
754     $word =~ s/ally$//;
755     $word =~ s/ly$//;
756     $word =~ s/ies$/y/;
757     $word =~ s/ally$/al/;
758     $word =~ s/izes$/ize/;
759     $word =~ tr/A-Z/a-z/;
760
761     if ( $word =~ s/[ \t\n\r]/\+/g ) {  # convert intra-word spaces to "+".
762       $word = "\%22$word\%22";          # And put quotes (%22) around it.
763     }
764
765     return $word;
766 }
767
768 sub random_words {
769   my ($or_p) = @_;
770   my $sep = ($or_p ? "%20OR%20" : "%20");
771   return (random_word . $sep .
772           random_word . $sep .
773           random_word . $sep .
774           random_word . $sep .
775           random_word);
776 }
777
778
779 sub url_quote {
780   my ($s) = @_;
781   $s =~ s|([^-a-zA-Z0-9.\@/_\r\n])|sprintf("%%%02X", ord($1))|ge;
782   return $s;
783 }
784
785 sub url_unquote {
786   my ($s) = @_;
787   $s =~ s/[+]/ /g;
788   $s =~ s/%([a-z0-9]{2})/chr(hex($1))/ige;
789   return $s;
790 }
791
792
793 # Loads the given URL (a search on some search engine) and returns:
794 # - the total number of hits the search engine claimed it had;
795 # - a list of URLs from the page that the search engine returned;
796 # Note that this list contains all kinds of internal search engine
797 # junk URLs too -- caller must prune them.
798 #
799 sub pick_from_search_engine {
800   my ( $timeout, $search_url, $words ) = @_;
801
802   $_ = $words;
803   s/%20/ /g;
804
805   print STDERR "\n\n" if ($verbose_load);
806
807   LOG ($verbose_load, "words: $_");
808   LOG ($verbose_load, "URL: $search_url");
809
810   $last_search = $search_url;   # for warnings
811
812   my $start = time;
813   my ( $base, $body ) = get_document ($search_url, undef, $timeout);
814   if (defined ($timeout)) {
815     $timeout -= (time - $start);
816     if ($timeout <= 0) {
817       $body = undef;
818       LOG (($verbose_net || $verbose_load),
819            "timed out (late) for $search_url");
820       $suppress_audit = 1;
821       return ();
822     }
823   }
824
825   return () if (! $body);
826
827
828   my @subpages;
829
830   my $search_count = "?";
831   if ($body =~ m@found (approximately |about )?(<B>)?(\d+)(</B>)? image@) {
832     $search_count = $3;
833   } elsif ($body =~ m@<NOBR>((\d{1,3})(,\d{3})*)&nbsp;@i) {
834     $search_count = $1;
835   } elsif ($body =~ m@found ((\d{1,3})(,\d{3})*|\d+) Web p@) {
836     $search_count = $1;
837   } elsif ($body =~ m@found about ((\d{1,3})(,\d{3})*|\d+) results@) {
838     $search_count = $1;
839   } elsif ($body =~ m@\b\d+ - \d+ of (\d+)\b@i) { # avimages
840     $search_count = $1;
841   } elsif ($body =~ m@About ((\d{1,3})(,\d{3})*) images@i) { # avimages
842     $search_count = $1;
843   } elsif ($body =~ m@We found ((\d{1,3})(,\d{3})*|\d+) results@i) { # *vista
844     $search_count = $1;
845   } elsif ($body =~ m@ of about <B>((\d{1,3})(,\d{3})*)<@i) { # googleimages
846     $search_count = $1;
847   } elsif ($body =~ m@<B>((\d{1,3})(,\d{3})*)</B> Web sites were found@i) {
848     $search_count = $1;    # lycos
849   } elsif ($body =~ m@WEB.*?RESULTS.*?\b((\d{1,3})(,\d{3})*)\b.*?Matches@i) {
850     $search_count = $1;                          # hotbot
851   } elsif ($body =~ m@no photos were found containing@i) { # avimages
852     $search_count = "0";
853   } elsif ($body =~ m@found no document matching@i) { # avtext
854     $search_count = "0";
855   }
856   1 while ($search_count =~ s/^(\d+)(\d{3})/$1,$2/);
857
858 #  if ($search_count eq "?" || $search_count eq "0") {
859 #    local *OUT;
860 #    my $file = "/tmp/wc.html";
861 #    open(OUT, ">$file") || error ("writing $file: $!");
862 #    print OUT $body;
863 #    close OUT;
864 #    print STDERR  blurb() . "###### wrote $file\n";
865 #  }
866
867
868   my $length = length($body);
869   my $href_count = 0;
870
871   $_ = $body;
872
873   s/[\r\n\t ]+/ /g;
874
875
876   s/(<A )/\n$1/gi;
877   foreach (split(/\n/)) {
878     $href_count++;
879     my ($u) = m@<A\s.*\bHREF\s*=\s*([^>]+)>@i;
880     next unless $u;
881
882     if ($u =~ m/^\"([^\"]*)\"/) { $u = $1; }   # quoted string
883     elsif ($u =~ m/^([^\s]*)\s/) { $u = $1; }  # or token
884
885     if ( $rejected_urls{$u} ) {
886       LOG ($verbose_filter, "  pre-rejecting candidate: $u");
887       next;
888     }
889
890     LOG ($verbose_http, "    HREF: $u");
891
892     $subpages[++$#subpages] = $u;
893   }
894
895   if ( $#subpages < 0 ) {
896     LOG ($verbose_filter,
897          "found nothing on $base ($length bytes, $href_count links).");
898     return ();
899   }
900
901   LOG ($verbose_filter, "" . $#subpages+1 . " links on $search_url");
902
903   return ($search_count, @subpages);
904 }
905
906
907 sub depoison {
908   my (@urls) = @_;
909   my @urls2 = ();
910   foreach (@urls) {
911     my ($h) = m@^http://([^/: \t\r\n]+)@i;
912
913     next unless defined($h);
914
915     if ($poisoners{$h}) {
916       LOG (($verbose_filter), "  rejecting poisoner: $_");
917       next;
918     }
919     if ($h =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
920         $poisoners{$1}) {
921       LOG (($verbose_filter), "  rejecting poisoner: $_");
922       next;
923     }
924     if ($h =~ m@([^.]+\.[^.]+)$@ &&
925         $poisoners{$1}) {
926       LOG (($verbose_filter), "  rejecting poisoner: $_");
927       next;
928     }
929
930     push @urls2, $_;
931   }
932   return @urls2;
933 }
934
935
936 # given a list of URLs, picks one at random; loads it; and returns a
937 # random image from it.
938 # returns the url of the page loaded; the url of the image chosen;
939 # and a debugging description string.
940 #
941 sub pick_image_from_pages {
942   my ($base, $total_hit_count, $unfiltered_link_count, $timeout, @pages) = @_;
943
944   $total_hit_count = "?" unless defined($total_hit_count);
945
946   @pages = depoison (@pages);
947   LOG ($verbose_load,
948        "" . ($#pages+1) . " candidates of $unfiltered_link_count links" .
949        " ($total_hit_count total)");
950
951   return () if ($#pages < 0);
952
953   my $i = int(rand($#pages+1));
954   my $page = $pages[$i];
955
956   LOG ($verbose_load, "picked page $page");
957
958   $suppress_audit = 1;
959
960   my ( $base2, $body2 ) = get_document ($page, $base, $timeout);
961
962   if (!$base2 || !$body2) {
963     $body2 = undef;
964     return ();
965   }
966
967   my $img = pick_image_from_body ($base2, $body2);
968   $body2 = undef;
969
970   if ($img) {
971     return ($base2, $img);
972   } else {
973     return ();
974   }
975 }
976
977 \f
978 ############################################################################
979 #
980 # Pick images from random pages returned by the Yahoo Random Link
981 #
982 ############################################################################
983
984 # yahoorand
985 my $yahoo_random_link = "http://random.yahoo.com/bin/ryl";
986
987
988 # Picks a random page; picks a random image on that page;
989 # returns two URLs: the page containing the image, and the image.
990 # Returns () if nothing found this time.
991 #
992 sub pick_from_yahoo_random_link {
993   my ( $timeout ) = @_;
994
995   print STDERR "\n\n" if ($verbose_load);
996   LOG ($verbose_load, "URL: $yahoo_random_link");
997
998   $last_search = $yahoo_random_link;   # for warnings
999
1000   $suppress_audit = 1;
1001
1002   my ( $base, $body ) = get_document ($yahoo_random_link, undef, $timeout);
1003   if (!$base || !$body) {
1004     $body = undef;
1005     return;
1006   }
1007
1008   LOG ($verbose_load, "redirected to: $base");
1009
1010   my $img = pick_image_from_body ($base, $body);
1011   $body = undef;
1012
1013   if ($img) {
1014     return ($base, $img);
1015   } else {
1016     return ();
1017   }
1018 }
1019
1020 \f
1021 ############################################################################
1022 #
1023 # Pick images from random pages returned by the Alta Vista Random Link
1024 #
1025 ############################################################################
1026
1027 # altavista
1028 my $alta_vista_random_link = "http://www.altavista.com/image/randomlink";
1029
1030
1031 # Picks a random page; picks a random image on that page;
1032 # returns two URLs: the page containing the image, and the image.
1033 # Returns () if nothing found this time.
1034 #
1035 sub pick_from_alta_vista_random_link {
1036   my ( $timeout ) = @_;
1037
1038   print STDERR "\n\n" if ($verbose_load);
1039   LOG ($verbose_load, "URL: $alta_vista_random_link");
1040
1041   $last_search = $alta_vista_random_link;   # for warnings
1042
1043   $suppress_audit = 1;
1044
1045   my ( $base, $body ) = get_document ($alta_vista_random_link,
1046                                       undef, $timeout);
1047   if (!$base || !$body) {
1048     $body = undef;
1049     return;
1050   }
1051
1052   LOG ($verbose_load, "redirected to: $base");
1053
1054   my $img = pick_image_from_body ($base, $body);
1055   $body = undef;
1056
1057   if ($img) {
1058     return ($base, $img);
1059   } else {
1060     return ();
1061   }
1062 }
1063
1064 \f
1065 ############################################################################
1066 #
1067 # Pick images by feeding random words into Alta Vista Image Search
1068 #
1069 ############################################################################
1070
1071
1072 my $alta_vista_images_url = "http://www.altavista.com/image/results" .
1073                             "?ipht=1" .       # photos
1074                             "&igrph=1" .      # graphics
1075                             "&iclr=1" .       # color
1076                             "&ibw=1" .        # b&w
1077                             "&micat=1" .      # no partner sites
1078                             "&sc=on" .        # "site collapse"
1079                             "&q=";
1080
1081 # avimages
1082 sub pick_from_alta_vista_images {
1083   my ( $timeout ) = @_;
1084
1085   my $words = random_words(0);
1086   my $page = (int(rand(9)) + 1);
1087   my $search_url = $alta_vista_images_url . $words;
1088
1089   if ($page > 1) {
1090     $search_url .= "&pgno=" . $page;            # page number
1091     $search_url .= "&stq=" . (($page-1) * 12);  # first hit result on page
1092   }
1093
1094   my ($search_hit_count, @subpages) =
1095     pick_from_search_engine ($timeout, $search_url, $words);
1096
1097   my @candidates = ();
1098   foreach my $u (@subpages) {
1099
1100     # avtext is encoding their URLs now.
1101     next unless ($u =~ m@^/r.*\&r=([^&]+).*@);
1102     $u = url_unquote($1);
1103
1104     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1105     next if ($u =~ m@[/.]altavista\.com\b@i);     # skip altavista builtins
1106     next if ($u =~ m@[/.]doubleclick\.net\b@i);   # you cretins
1107     next if ($u =~ m@[/.]clicktomarket\.com\b@i); # more cretins
1108
1109     next if ($u =~ m@[/.]viewimages\.com\b@i);    # stacked deck
1110     next if ($u =~ m@[/.]gettyimages\.com\b@i);
1111
1112     LOG ($verbose_filter, "  candidate: $u");
1113     push @candidates, $u;
1114   }
1115
1116   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1117                                 $timeout, @candidates);
1118 }
1119
1120
1121 \f
1122 ############################################################################
1123 #
1124 # Pick images by feeding random words into Google Image Search.
1125 # By Charles Gales <gales@us.ibm.com>
1126 #
1127 ############################################################################
1128
1129
1130 my $google_images_url =     "http://images.google.com/images" .
1131                             "?site=images" .  # photos
1132                             "&btnG=Search" .  # graphics
1133                             "&safe=off" .     # no screening
1134                             "&imgsafe=off" .
1135                             "&q=";
1136
1137 # googleimgs
1138 sub pick_from_google_images {
1139   my ( $timeout ) = @_;
1140
1141   my $words = random_word;   # only one word for Google
1142   my $page = (int(rand(9)) + 1);
1143   my $num = 20;     # 20 images per page
1144   my $search_url = $google_images_url . $words;
1145
1146   if ($page > 1) {
1147     $search_url .= "&start=" . $page*$num;      # page number
1148     $search_url .= "&num="   . $num;            #images per page
1149   }
1150
1151   my ($search_hit_count, @subpages) =
1152     pick_from_search_engine ($timeout, $search_url, $words);
1153
1154   my @candidates = ();
1155   foreach my $u (@subpages) {
1156     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
1157     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
1158
1159     if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
1160       my $urlf = $2;
1161       LOG ($verbose_filter, "  candidate: $urlf");
1162       push @candidates, $urlf;
1163     }
1164   }
1165
1166   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1167                                 $timeout, @candidates);
1168 }
1169
1170
1171 \f
1172 ############################################################################
1173 #
1174 # Pick images by feeding random *numbers* into Google Image Search.
1175 # By jwz, suggested by from Ian O'Donnell.
1176 #
1177 ############################################################################
1178
1179
1180 # googlenums
1181 sub pick_from_google_image_numbers {
1182   my ( $timeout ) = @_;
1183
1184   my $max = 9999;
1185   my $number = int(rand($max));
1186
1187   $number = sprintf("%04d", $number)
1188     if (rand() < 0.3);
1189
1190   my $words = "$number";
1191   my $page = (int(rand(40)) + 1);
1192   my $num = 20;     # 20 images per page
1193   my $search_url = $google_images_url . $words;
1194
1195   if ($page > 1) {
1196     $search_url .= "&start=" . $page*$num;      # page number
1197     $search_url .= "&num="   . $num;            #images per page
1198   }
1199
1200   my ($search_hit_count, @subpages) =
1201     pick_from_search_engine ($timeout, $search_url, $words);
1202
1203   my @candidates = ();
1204   my %referers;
1205   foreach my $u (@subpages) {
1206     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
1207     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
1208
1209     if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
1210       my $ref = $2;
1211       my $img = "http://$1";
1212
1213       LOG ($verbose_filter, "  candidate: $ref");
1214       push @candidates, $img;
1215       $referers{$img} = $ref;
1216     }
1217   }
1218
1219   @candidates = depoison (@candidates);
1220   return () if ($#candidates < 0);
1221   my $i = int(rand($#candidates+1));
1222   my $img = $candidates[$i];
1223   my $ref = $referers{$img};
1224
1225   LOG ($verbose_load, "picked image " . ($i+1) . ": $img (on $ref)");
1226   return ($ref, $img);
1227 }
1228
1229
1230 \f
1231 ############################################################################
1232 #
1233 # Pick images by feeding random words into Alta Vista Text Search
1234 #
1235 ############################################################################
1236
1237
1238 my $alta_vista_url = "http://www.altavista.com/web/results" .
1239                      "?pg=aq" .
1240                      "&aqmode=s" .
1241                      "&filetype=html" .
1242                      "&sc=on" .        # "site collapse"
1243                      "&nbq=50" .
1244                      "&aqo=";
1245
1246 # avtext
1247 sub pick_from_alta_vista_text {
1248   my ( $timeout ) = @_;
1249
1250   my $words = random_words(0);
1251   my $page = (int(rand(9)) + 1);
1252   my $search_url = $alta_vista_url . $words;
1253
1254   if ($page > 1) {
1255     $search_url .= "&pgno=" . $page;
1256     $search_url .= "&stq=" . (($page-1) * 10);
1257   }
1258
1259   my ($search_hit_count, @subpages) =
1260     pick_from_search_engine ($timeout, $search_url, $words);
1261
1262   my @candidates = ();
1263   foreach my $u (@subpages) {
1264
1265     # Those altavista fuckers are playing really nasty redirection games
1266     # these days: the filter your clicks through their site, but use
1267     # onMouseOver to make it look like they're not!  Well, it makes it
1268     # easier for us to identify search results...
1269     #
1270     next unless ($u =~ m@^/r.*\&r=([^&]+).*@);
1271     $u = url_unquote($1);
1272
1273     LOG ($verbose_filter, "  candidate: $u");
1274     push @candidates, $u;
1275   }
1276
1277   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1278                                 $timeout, @candidates);
1279 }
1280
1281
1282 \f
1283 ############################################################################
1284 #
1285 # Pick images by feeding random words into Hotbot
1286 #
1287 ############################################################################
1288
1289 my $hotbot_search_url =("http://hotbot.lycos.com/default.asp" .
1290                         "?ca=w" .
1291                         "&descriptiontype=0" .
1292                         "&imagetoggle=1" .
1293                         "&matchmode=any" .
1294                         "&nummod=2" .
1295                         "&recordcount=50" .
1296                         "&sitegroup=1" .
1297                         "&stem=1" .
1298                         "&cobrand=undefined" .
1299                         "&query=");
1300
1301 sub pick_from_hotbot_text {
1302   my ( $timeout ) = @_;
1303
1304   # lycos seems to always give us back dictionaries and word lists if
1305   # we search for more than one word...
1306   #
1307   my $words = random_word();
1308
1309   my $start = int(rand(8)) * 10 + 1;
1310   my $search_url = $hotbot_search_url . $words . "&first=$start&page=more";
1311
1312   my ($search_hit_count, @subpages) =
1313     pick_from_search_engine ($timeout, $search_url, $words);
1314
1315   my @candidates = ();
1316   foreach my $u (@subpages) {
1317
1318     # Hotbot plays redirection games too
1319     next unless ($u =~ m@/director.asp\?.*\btarget=([^&]+)@);
1320     $u = url_decode($1);
1321
1322     LOG ($verbose_filter, "  candidate: $u");
1323     push @candidates, $u;
1324   }
1325
1326   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1327                                 $timeout, @candidates);
1328 }
1329
1330
1331 \f
1332 ############################################################################
1333 #
1334 # Pick images by feeding random words into Lycos
1335 #
1336 ############################################################################
1337
1338 my $lycos_search_url = "http://search.lycos.com/default.asp" .
1339                        "?lpv=1" .
1340                        "&loc=searchhp" .
1341                        "&tab=web" .
1342                        "&query=";
1343
1344 sub pick_from_lycos_text {
1345   my ( $timeout ) = @_;
1346
1347   # lycos seems to always give us back dictionaries and word lists if
1348   # we search for more than one word...
1349   #
1350   my $words = random_word();
1351
1352   my $start = int(rand(8)) * 10 + 1;
1353   my $search_url = $lycos_search_url . $words . "&first=$start&page=more";
1354
1355   my ($search_hit_count, @subpages) =
1356     pick_from_search_engine ($timeout, $search_url, $words);
1357
1358   my @candidates = ();
1359   foreach my $u (@subpages) {
1360
1361     # Lycos plays redirection games.
1362     next unless ($u =~ m@^http://click.lycos.com/director.asp
1363                          .*
1364                          \btarget=([^&]+)
1365                          .*
1366                         @x);
1367     $u = url_decode($1);
1368
1369     LOG ($verbose_filter, "  candidate: $u");
1370     push @candidates, $u;
1371   }
1372
1373   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1374                                 $timeout, @candidates);
1375 }
1376
1377
1378 \f
1379 ############################################################################
1380 #
1381 # Pick images by feeding random words into news.yahoo.com
1382 #
1383 ############################################################################
1384
1385 my $yahoo_news_url = "http://search.news.yahoo.com/search/news" .
1386                      "?a=1" .
1387                      "&c=news_photos" .
1388                      "&s=-%24s%2C-date" .
1389                      "&n=100" .
1390                      "&o=o" .
1391                      "&2=" .
1392                      "&3=" .
1393                      "&p=";
1394
1395 # yahoonews
1396 sub pick_from_yahoo_news_text {
1397   my ( $timeout ) = @_;
1398
1399   my $words = random_words(0);
1400   my $search_url = $yahoo_news_url . $words;
1401
1402   my ($search_hit_count, @subpages) =
1403     pick_from_search_engine ($timeout, $search_url, $words);
1404
1405   my @candidates = ();
1406   foreach my $u (@subpages) {
1407     # only accept URLs on Yahoo's news site
1408     next unless ($u =~ m@^http://dailynews\.yahoo\.com/@i ||
1409                  $u =~ m@^http://story\.news\.yahoo\.com/@i);
1410
1411     LOG ($verbose_filter, "  candidate: $u");
1412     push @candidates, $u;
1413   }
1414
1415   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1416                                 $timeout, @candidates);
1417 }
1418
1419
1420
1421 \f
1422 ############################################################################
1423 #
1424 # Pick images by waiting for driftnet to populate a temp dir with files.
1425 # Requires driftnet version 0.1.5 or later.
1426 # (Driftnet is a program by Chris Lightfoot that sniffs your local ethernet
1427 # for images being downloaded by others.)
1428 # Driftnet/webcollage integration by jwz.
1429 #
1430 ############################################################################
1431
1432 # driftnet
1433 sub pick_from_driftnet {
1434   my ( $timeout ) = @_;
1435
1436   my $id = $driftnet_magic;
1437   my $dir = $driftnet_dir;
1438   my $start = time;
1439   my $now;
1440
1441   error ("\$driftnet_dir unset?") unless ($dir);
1442   $dir =~ s@/+$@@;
1443
1444   error ("$dir unreadable") unless (-d "$dir/.");
1445
1446   $timeout = $http_timeout unless ($timeout);
1447   $last_search = $id;
1448
1449   while ($now = time, $now < $start + $timeout) {
1450     local *DIR;
1451     opendir (DIR, $dir) || error ("$dir: $!");
1452     while (my $file = readdir(DIR)) {
1453       next if ($file =~ m/^\./);
1454       $file = "$dir/$file";
1455       closedir DIR;
1456       LOG ($verbose_load, "picked file $file ($id)");
1457       return ($id, $file);
1458     }
1459     closedir DIR;
1460   }
1461   LOG (($verbose_net || $verbose_load), "timed out for $id");
1462   return ();
1463 }
1464
1465
1466 sub get_driftnet_file {
1467   my ($file) = @_;
1468
1469   error ("\$driftnet_dir unset?") unless ($driftnet_dir);
1470
1471   my $id = $driftnet_magic;
1472   my $re = qr/$driftnet_dir/;
1473   error ("$id: $file not in $driftnet_dir?")
1474     unless ($file =~ m@^$re@o);
1475
1476   local *IN;
1477   open (IN, $file) || error ("$id: $file: $!");
1478   my $body = '';
1479   while (<IN>) { $body .= $_; }
1480   close IN || error ("$id: $file: $!");
1481   unlink ($file) || error ("$id: $file: rm: $!");
1482   return ($id, $body);
1483 }
1484
1485
1486 sub spawn_driftnet {
1487   my ($cmd) = @_;
1488
1489   # make a directory to use.
1490   while (1) {
1491     my $tmp = $ENV{TEMPDIR} || "/tmp";
1492     $driftnet_dir = sprintf ("$tmp/driftcollage-%08x", rand(0xffffffff));
1493     LOG ($verbose_exec, "mkdir $driftnet_dir");
1494     last if mkdir ($driftnet_dir, 0700);
1495   }
1496
1497   if (! ($cmd =~ m/\s/)) {
1498     # if the command didn't have any arguments in it, then it must be just
1499     # a pointer to the executable.  Append the default args to it.
1500     my $dargs = $default_driftnet_cmd;
1501     $dargs =~ s/^[^\s]+//;
1502     $cmd .= $dargs;
1503   }
1504
1505   # point the driftnet command at our newly-minted private directory.
1506   #
1507   $cmd .= " -d $driftnet_dir";
1508   $cmd .= ">/dev/null" unless ($verbose_exec);
1509
1510   my $pid = fork();
1511   if ($pid < 0) { error ("fork: $!\n"); }
1512   if ($pid) {
1513     # parent fork
1514     push @pids_to_kill, $pid;
1515     LOG ($verbose_exec, "forked for \"$cmd\"");
1516   } else {
1517     # child fork
1518     nontrapping_system ($cmd) || error ("exec: $!");
1519   }
1520
1521   # wait a bit, then make sure the process actually started up.
1522   #
1523   sleep (1);
1524   error ("pid $pid failed to start \"$cmd\"")
1525     unless (1 == kill (0, $pid));
1526 }
1527
1528 \f
1529 ############################################################################
1530 #
1531 # Pick a random image in a random way
1532 #
1533 ############################################################################
1534
1535
1536 # Picks a random image on a random page, and returns two URLs:
1537 # the page containing the image, and the image.
1538 # Returns () if nothing found this time.
1539 #
1540
1541 sub pick_image {
1542   my ( $timeout ) = @_;
1543
1544   $current_state = "select";
1545   $load_method = "none";
1546
1547   my $n = int(rand(100));
1548   my $fn = undef;
1549   my $total = 0;
1550   my @rest = @search_methods;
1551
1552   while (@rest) {
1553     my $pct  = shift @rest;
1554     my $name = shift @rest;
1555     my $tfn  = shift @rest;
1556     $total += $pct;
1557     if ($total > $n && !defined($fn)) {
1558       $fn = $tfn;
1559       $current_state = $name;
1560       $load_method = $current_state;
1561     }
1562   }
1563
1564   if ($total != 100) {
1565     error ("internal error: \@search_methods totals to $total%!");
1566   }
1567
1568   record_attempt ($current_state);
1569   return $fn->($timeout);
1570 }
1571
1572
1573 \f
1574 ############################################################################
1575 #
1576 # Statistics and logging
1577 #
1578 ############################################################################
1579
1580 sub timestr {
1581   return strftime ("%H:%M:%S: ", localtime);
1582 }
1583
1584 sub blurb {
1585   return "$progname: " . timestr() . "$current_state: ";
1586 }
1587
1588 sub error {
1589   my ($err) = @_;
1590   print STDERR blurb() . "$err\n";
1591   exit 1;
1592 }
1593
1594
1595 my $lastlog = "";
1596
1597 sub clearlog {
1598   $lastlog = "";
1599 }
1600
1601 sub showlog {
1602   my $head = "$progname: DEBUG: ";
1603   foreach (split (/\n/, $lastlog)) {
1604     print STDERR "$head$_\n";
1605   }
1606   $lastlog = "";
1607 }
1608
1609 sub LOG {
1610   my ($print, $msg) = @_;
1611   my $blurb = timestr() . "$current_state: ";
1612   $lastlog .= "$blurb$msg\n";
1613   print STDERR "$progname: $blurb$msg\n" if $print;
1614 }
1615
1616
1617 my %stats_attempts;
1618 my %stats_successes;
1619 my %stats_elapsed;
1620
1621 my $last_state = undef;
1622 sub record_attempt {
1623   my ($name) = @_;
1624
1625   if ($last_state) {
1626     record_failure($last_state) unless ($image_succeeded > 0);
1627   }
1628   $last_state = $name;
1629
1630   clearlog();
1631   report_performance();
1632
1633   start_timer($name);
1634   $image_succeeded = 0;
1635   $suppress_audit = 0;
1636 }
1637
1638 sub record_success {
1639   my ($name, $url, $base) = @_;
1640   if (defined($stats_successes{$name})) {
1641     $stats_successes{$name}++;
1642   } else {
1643     $stats_successes{$name} = 1;
1644   }
1645
1646   stop_timer ($name, 1);
1647   my $o = $current_state;
1648   $current_state = $name;
1649   save_recent_url ($url, $base);
1650   $current_state = $o;
1651   $image_succeeded = 1;
1652   clearlog();
1653 }
1654
1655
1656 sub record_failure {
1657   my ($name) = @_;
1658
1659   return if $image_succeeded;
1660
1661   stop_timer ($name, 0);
1662   if ($verbose_load && !$verbose_exec) {
1663
1664     if ($suppress_audit) {
1665       print STDERR "$progname: " . timestr() . "(audit log suppressed)\n";
1666       return;
1667     }
1668
1669     my $o = $current_state;
1670     $current_state = "DEBUG";
1671
1672     my $line =  "#" x 78;
1673     print STDERR "\n\n\n";
1674     print STDERR ("#" x 78) . "\n";
1675     print STDERR blurb() . "failed to get an image.  Full audit log:\n";
1676     print STDERR "\n";
1677     showlog();
1678     print STDERR ("-" x 78) . "\n";
1679     print STDERR "\n\n";
1680
1681     $current_state = $o;
1682   }
1683   $image_succeeded = 0;
1684 }
1685
1686
1687
1688 sub stats_of {
1689   my ($name) = @_;
1690   my $i = $stats_successes{$name};
1691   my $j = $stats_attempts{$name};
1692   $i = 0 unless $i;
1693   $j = 0 unless $j;
1694   return "" . ($j ? int($i * 100 / $j) : "0") . "%";
1695 }
1696
1697
1698 my $current_start_time = 0;
1699
1700 sub start_timer {
1701   my ($name) = @_;
1702   $current_start_time = time;
1703
1704   if (defined($stats_attempts{$name})) {
1705     $stats_attempts{$name}++;
1706   } else {
1707     $stats_attempts{$name} = 1;
1708   }
1709   if (!defined($stats_elapsed{$name})) {
1710     $stats_elapsed{$name} = 0;
1711   }
1712 }
1713
1714 sub stop_timer {
1715   my ($name, $success) = @_;
1716   $stats_elapsed{$name} += time - $current_start_time;
1717 }
1718
1719
1720 my $last_report_time = 0;
1721 sub report_performance {
1722
1723   return unless $verbose_warnings;
1724
1725   my $now = time;
1726   return unless ($now >= $last_report_time + $report_performance_interval);
1727   my $ot = $last_report_time;
1728   $last_report_time = $now;
1729
1730   return if ($ot == 0);
1731
1732   my $blurb = "$progname: " . timestr();
1733
1734   print STDERR "\n";
1735   print STDERR "${blurb}Current standings:\n";
1736
1737   foreach my $name (sort keys (%stats_attempts)) {
1738     my $try = $stats_attempts{$name};
1739     my $suc = $stats_successes{$name} || 0;
1740     my $pct = int($suc * 100 / $try);
1741     my $secs = $stats_elapsed{$name};
1742     my $secs_link = int($secs / $try);
1743     print STDERR sprintf ("$blurb   %-12s %4s (%d/%d);\t %2d secs/link\n",
1744                           "$name:", "$pct%", $suc, $try, $secs_link);
1745   }
1746 }
1747
1748
1749
1750 my $max_recent_images = 400;
1751 my $max_recent_sites  = 20;
1752 my @recent_images = ();
1753 my @recent_sites = ();
1754
1755 sub save_recent_url {
1756   my ($url, $base) = @_;
1757
1758   return unless ($verbose_warnings);
1759
1760   $_ = $url;
1761   my ($site) = m@^http://([^ \t\n\r/:]+)@;
1762   return unless defined ($site);
1763
1764   if ($base eq $driftnet_magic) {
1765     $site = $driftnet_magic;
1766     @recent_images = ();
1767   }
1768
1769   my $done = 0;
1770   foreach (@recent_images) {
1771     if ($_ eq $url) {
1772       print STDERR blurb() . "WARNING: recently-duplicated image: $url" .
1773         " (on $base via $last_search)\n";
1774       $done = 1;
1775       last;
1776     }
1777   }
1778
1779   # suppress "duplicate site" warning via %warningless_sites.
1780   #
1781   if ($warningless_sites{$site}) {
1782     $done = 1;
1783   } elsif ($site =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
1784            $warningless_sites{$1}) {
1785     $done = 1;
1786   } elsif ($site =~ m@([^.]+\.[^.]+)$@ &&
1787            $warningless_sites{$1}) {
1788     $done = 1;
1789   }
1790
1791   if (!$done) {
1792     foreach (@recent_sites) {
1793       if ($_ eq $site) {
1794         print STDERR blurb() . "WARNING: recently-duplicated site: $site" .
1795         " ($url on $base via $last_search)\n";
1796         last;
1797       }
1798     }
1799   }
1800
1801   push @recent_images, $url;
1802   push @recent_sites,  $site;
1803   shift @recent_images if ($#recent_images >= $max_recent_images);
1804   shift @recent_sites  if ($#recent_sites  >= $max_recent_sites);
1805 }
1806
1807
1808 \f
1809 ##############################################################################
1810 #
1811 # other utilities
1812 #
1813 ##############################################################################
1814
1815 # Does %-decoding.
1816 #
1817 sub url_decode {
1818   ($_) = @_;
1819   tr/+/ /;
1820   s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
1821   return $_;
1822 }
1823
1824
1825 # Given the raw body of a GIF document, returns the dimensions of the image.
1826 #
1827 sub gif_size {
1828   my ($body) = @_;
1829   my $type = substr($body, 0, 6);
1830   my $s;
1831   return () unless ($type =~ /GIF8[7,9]a/);
1832   $s = substr ($body, 6, 10);
1833   my ($a,$b,$c,$d) = unpack ("C"x4, $s);
1834   return (($b<<8|$a), ($d<<8|$c));
1835 }
1836
1837 # Given the raw body of a JPEG document, returns the dimensions of the image.
1838 #
1839 sub jpeg_size {
1840   my ($body) = @_;
1841   my $i = 0;
1842   my $L = length($body);
1843
1844   my $c1 = substr($body, $i, 1); $i++;
1845   my $c2 = substr($body, $i, 1); $i++;
1846   return () unless (ord($c1) == 0xFF && ord($c2) == 0xD8);
1847
1848   my $ch = "0";
1849   while (ord($ch) != 0xDA && $i < $L) {
1850     # Find next marker, beginning with 0xFF.
1851     while (ord($ch) != 0xFF) {
1852       return () if (length($body) <= $i);
1853       $ch = substr($body, $i, 1); $i++;
1854     }
1855     # markers can be padded with any number of 0xFF.
1856     while (ord($ch) == 0xFF) {
1857       return () if (length($body) <= $i);
1858       $ch = substr($body, $i, 1); $i++;
1859     }
1860
1861     # $ch contains the value of the marker.
1862     my $marker = ord($ch);
1863
1864     if (($marker >= 0xC0) &&
1865         ($marker <= 0xCF) &&
1866         ($marker != 0xC4) &&
1867         ($marker != 0xCC)) {  # it's a SOFn marker
1868       $i += 3;
1869       return () if (length($body) <= $i);
1870       my $s = substr($body, $i, 4); $i += 4;
1871       my ($a,$b,$c,$d) = unpack("C"x4, $s);
1872       return (($c<<8|$d), ($a<<8|$b));
1873
1874     } else {
1875       # We must skip variables, since FFs in variable names aren't
1876       # valid JPEG markers.
1877       return () if (length($body) <= $i);
1878       my $s = substr($body, $i, 2); $i += 2;
1879       my ($c1, $c2) = unpack ("C"x2, $s);
1880       my $length = ($c1 << 8) | $c2;
1881       return () if ($length < 2);
1882       $i += $length-2;
1883     }
1884   }
1885   return ();
1886 }
1887
1888 # Given the raw body of a GIF or JPEG document, returns the dimensions of
1889 # the image.
1890 #
1891 sub image_size {
1892   my ($body) = @_;
1893   my ($w, $h) = gif_size ($body);
1894   if ($w && $h) { return ($w, $h); }
1895   return jpeg_size ($body);
1896 }
1897
1898
1899 # returns the full path of the named program, or undef.
1900 #
1901 sub which {
1902   my ($prog) = @_;
1903   foreach (split (/:/, $ENV{PATH})) {
1904     if (-x "$_/$prog") {
1905       return $prog;
1906     }
1907   }
1908   return undef;
1909 }
1910
1911
1912 # Like rand(), but chooses numbers with a bell curve distribution.
1913 sub bellrand {
1914   ($_) = @_;
1915   $_ = 1.0 unless defined($_);
1916   $_ /= 3.0;
1917   return (rand($_) + rand($_) + rand($_));
1918 }
1919
1920
1921 sub signal_cleanup {
1922   my ($sig) = @_;
1923   print STDERR blurb() . (defined($sig)
1924                           ? "caught signal $sig."
1925                           : "exiting.")
1926                        . "\n"
1927     if ($verbose_exec);
1928
1929   x_cleanup();
1930
1931   if (@pids_to_kill) {
1932     print STDERR blurb() . "killing: " . join(' ', @pids_to_kill) . "\n";
1933     kill ('TERM', @pids_to_kill);
1934   }
1935
1936   exit 1;
1937 }
1938
1939 ##############################################################################
1940 #
1941 # Generating a list of urls only
1942 #
1943 ##############################################################################
1944
1945 sub url_only_output {
1946   do {
1947     my ($base, $img) = pick_image;
1948     if ($img) {
1949       $base =~ s/ /%20/g;
1950       $img  =~ s/ /%20/g;
1951       print "$img $base\n";
1952     }
1953   } while (1);
1954 }
1955
1956 ##############################################################################
1957 #
1958 # Running as an xscreensaver module
1959 #
1960 ##############################################################################
1961
1962 my $image_ppm   = ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp") . "/webcollage." . $$;
1963 my $image_tmp1  = $image_ppm . "-1";
1964 my $image_tmp2  = $image_ppm . "-2";
1965
1966 my $filter_cmd = undef;
1967 my $post_filter_cmd = undef;
1968 my $background = undef;
1969
1970 my $img_width;            # size of the image being generated.
1971 my $img_height;
1972
1973 my $delay = 2;
1974
1975 sub x_cleanup {
1976   unlink $image_ppm, $image_tmp1, $image_tmp2;
1977 }
1978
1979
1980 # Like system, but prints status about exit codes, and kills this process
1981 # with whatever signal killed the sub-process, if any.
1982 #
1983 sub nontrapping_system {
1984   $! = 0;
1985
1986   $_ = join(" ", @_);
1987   s/\"[^\"]+\"/\"...\"/g;
1988
1989   LOG ($verbose_exec, "executing \"$_\"");
1990
1991   my $rc = system @_;
1992
1993   if ($rc == 0) {
1994     LOG ($verbose_exec, "subproc exited normally.");
1995   } elsif (($rc & 0xff) == 0) {
1996     $rc >>= 8;
1997     LOG ($verbose_exec, "subproc exited with status $rc.");
1998   } else {
1999     if ($rc & 0x80) {
2000       LOG ($verbose_exec, "subproc dumped core.");
2001       $rc &= ~0x80;
2002     }
2003     LOG ($verbose_exec, "subproc died with signal $rc.");
2004     # die that way ourselves.
2005     kill $rc, $$;
2006   }
2007
2008   return $rc;
2009 }
2010
2011
2012 # Given the URL of a GIF or JPEG image, and the body of that image, writes a
2013 # PPM to the given output file.  Returns the width/height of the image if
2014 # successful.
2015 #
2016 sub image_to_pnm {
2017   my ($url, $body, $output) = @_;
2018   my ($cmd, $cmd2, $w, $h);
2019
2020   if ((@_ = gif_size ($body))) {
2021     ($w, $h) = @_;
2022     $cmd = "giftopnm";
2023   } elsif ((@_ = jpeg_size ($body))) {
2024     ($w, $h) = @_;
2025     $cmd = "djpeg";
2026   } else {
2027     LOG (($verbose_pbm || $verbose_load),
2028          "not a GIF or JPG" .
2029          (($body =~ m@<(base|html|head|body|script|table|a href)>@i)
2030           ? " (looks like HTML)" : "") .
2031          ": $url");
2032     $suppress_audit = 1;
2033     return ();
2034   }
2035
2036   $cmd2 = "exec $cmd";        # yes, this really is necessary.  if we don't
2037                               # do this, the process doesn't die properly.
2038   if (!$verbose_pbm) {
2039     #
2040     # We get a "giftopnm: got a 'Application Extension' extension"
2041     # warning any time it's an animgif.
2042     #
2043     # Note that "giftopnm: EOF / read error on image data" is not
2044     # always a fatal error -- sometimes the image looks fine anyway.
2045     #
2046     $cmd2 .= " 2>/dev/null";
2047   }
2048
2049   # There exist corrupted GIF and JPEG files that can make giftopnm and
2050   # djpeg lose their minds and go into a loop.  So this gives those programs
2051   # a small timeout -- if they don't complete in time, kill them.
2052   #
2053   my $pid;
2054   @_ = eval {
2055     my $timed_out;
2056
2057     local $SIG{ALRM}  = sub {
2058       LOG ($verbose_pbm,
2059            "timed out ($cvt_timeout) for $cmd on \"$url\" in pid $pid");
2060       kill ('TERM', $pid) if ($pid);
2061       $timed_out = 1;
2062       $body = undef;
2063     };
2064
2065     if (($pid = open(PIPE, "| $cmd2 > $output"))) {
2066       $timed_out = 0;
2067       alarm $cvt_timeout;
2068       print PIPE $body;
2069       $body = undef;
2070       close PIPE;
2071
2072       LOG ($verbose_exec, "awaiting $pid");
2073       waitpid ($pid, 0);
2074       LOG ($verbose_exec, "$pid completed");
2075
2076       my $size = (stat($output))[7];
2077       $size = -1 unless defined($size);
2078       if ($size < 5) {
2079         LOG ($verbose_pbm, "$cmd on ${w}x$h \"$url\" failed ($size bytes)");
2080         return ();
2081       }
2082
2083       LOG ($verbose_pbm, "created ${w}x$h $output ($cmd)");
2084       return ($w, $h);
2085     } else {
2086       print STDERR blurb() . "$cmd failed: $!\n";
2087       return ();
2088     }
2089   };
2090   die if ($@ && $@ ne "alarm\n");       # propagate errors
2091   if ($@) {
2092     # timed out
2093     $body = undef;
2094     return ();
2095   } else {
2096     # didn't
2097     alarm 0;
2098     $body = undef;
2099     return @_;
2100   }
2101 }
2102
2103 sub pick_root_displayer {
2104   my @names = ();
2105
2106   foreach my $cmd (@root_displayers) {
2107     $_ = $cmd;
2108     my ($name) = m/^([^ ]+)/;
2109     push @names, "\"$name\"";
2110     LOG ($verbose_exec, "looking for $name...");
2111     foreach my $dir (split (/:/, $ENV{PATH})) {
2112       LOG ($verbose_exec, "  checking $dir/$name");
2113       return $cmd if (-x "$dir/$name");
2114     }
2115   }
2116
2117   $names[$#names] = "or " . $names[$#names];
2118   error "none of: " . join (", ", @names) . " were found on \$PATH.";
2119 }
2120
2121
2122 my $ppm_to_root_window_cmd = undef;
2123
2124
2125 sub x_or_pbm_output {
2126
2127   # Check for our helper program, to see whether we need to use PPM pipelines.
2128   #
2129   $_ = "webcollage-helper";
2130   if (defined ($webcollage_helper) || which ($_)) {
2131     $webcollage_helper = $_ unless (defined($webcollage_helper));
2132     LOG ($verbose_pbm, "found \"$webcollage_helper\"");
2133     $webcollage_helper .= " -v";
2134   } else {
2135     LOG (($verbose_pbm || $verbose_load), "no $_ program");
2136   }
2137
2138   # make sure the various programs we execute exist, right up front.
2139   #
2140   my @progs = ("ppmmake");  # always need this one
2141
2142   if (!defined($webcollage_helper)) {
2143     # Only need these others if we don't have the helper.
2144     @progs = (@progs, "giftopnm", "djpeg", "pnmpaste", "pnmscale", "pnmcut");
2145   }
2146
2147   foreach (@progs) {
2148     which ($_) || error "$_ not found on \$PATH.";
2149   }
2150
2151   # find a root-window displayer program.
2152   #
2153   $ppm_to_root_window_cmd = pick_root_displayer();
2154
2155   if (!$img_width || !$img_height) {
2156     $_ = "xdpyinfo";
2157     which ($_) || error "$_ not found on \$PATH.";
2158     $_ = `$_`;
2159     ($img_width, $img_height) = m/dimensions: *(\d+)x(\d+) /;
2160     if (!defined($img_height)) {
2161       error "xdpyinfo failed.";
2162     }
2163   }
2164
2165   my $bgcolor = "#000000";
2166   my $bgimage = undef;
2167
2168   if ($background) {
2169     if ($background =~ m/^\#[0-9a-f]+$/i) {
2170       $bgcolor = $background;
2171
2172     } elsif (-r $background) {
2173       $bgimage = $background;
2174
2175     } elsif (! $background =~ m@^[-a-z0-9 ]+$@i) {
2176       error "not a color or readable file: $background";
2177
2178     } else {
2179       # default to assuming it's a color
2180       $bgcolor = $background;
2181     }
2182   }
2183
2184   # Create the sold-colored base image.
2185   #
2186   $_ = "ppmmake '$bgcolor' $img_width $img_height";
2187   LOG ($verbose_pbm, "creating base image: $_");
2188   nontrapping_system "$_ > $image_ppm";
2189
2190   # Paste the default background image in the middle of it.
2191   #
2192   if ($bgimage) {
2193     my ($iw, $ih);
2194
2195     my $body = "";
2196     local *IMG;
2197     open(IMG, "<$bgimage") || error "couldn't open $bgimage: $!";
2198     my $cmd;
2199     while (<IMG>) { $body .= $_; }
2200     close (IMG);
2201
2202     if ((@_ = gif_size ($body))) {
2203       ($iw, $ih) = @_;
2204       $cmd = "giftopnm |";
2205
2206     } elsif ((@_ = jpeg_size ($body))) {
2207       ($iw, $ih) = @_;
2208       $cmd = "djpeg |";
2209
2210     } elsif ($body =~ m/^P\d\n(\d+) (\d+)\n/) {
2211       $iw = $1;
2212       $ih = $2;
2213       $cmd = "";
2214
2215     } else {
2216       error "$bgimage is not a GIF, JPEG, or PPM.";
2217     }
2218
2219     my $x = int (($img_width  - $iw) / 2);
2220     my $y = int (($img_height - $ih) / 2);
2221     LOG ($verbose_pbm,
2222          "pasting $bgimage (${iw}x$ih) into base image at $x,$y");
2223
2224     $cmd .= "pnmpaste - $x $y $image_ppm > $image_tmp1";
2225     open (IMG, "| $cmd") || error "running $cmd: $!";
2226     print IMG $body;
2227     $body = undef;
2228     close (IMG);
2229     LOG ($verbose_exec, "subproc exited normally.");
2230     rename ($image_tmp1, $image_ppm) ||
2231       error "renaming $image_tmp1 to $image_ppm: $!";
2232   }
2233
2234   clearlog();
2235
2236   while (1) {
2237     my ($base, $img) = pick_image();
2238     my $source = $current_state;
2239     $current_state = "loadimage";
2240     if ($img) {
2241       my ($headers, $body) = get_document ($img, $base);
2242       if ($body) {
2243         paste_image ($base, $img, $body, $source);
2244         $body = undef;
2245       }
2246     }
2247     $current_state = "idle";
2248     $load_method = "none";
2249
2250     unlink $image_tmp1, $image_tmp2;
2251     sleep $delay;
2252   }
2253 }
2254
2255 sub paste_image {
2256   my ($base, $img, $body, $source) = @_;
2257
2258   $current_state = "paste";
2259
2260   $suppress_audit = 0;
2261
2262   LOG ($verbose_pbm, "got $img (" . length($body) . ")");
2263
2264   my ($iw, $ih);
2265
2266   # If we are using the webcollage-helper, then we do not need to convert this
2267   # image to a PPM.  But, if we're using a filter command, we still must, since
2268   # that's what the filters expect (webcollage-helper can read PPMs, so that's
2269   # fine.)
2270   #
2271   if (defined ($webcollage_helper) &&
2272       !defined ($filter_cmd)) {
2273
2274     ($iw, $ih) = image_size ($body);
2275     if (!$iw || !$ih) {
2276       LOG (($verbose_pbm || $verbose_load),
2277            "not a GIF or JPG" .
2278            (($body =~ m@<(base|html|head|body|script|table|a href)>@i)
2279             ? " (looks like HTML)" : "") .
2280            ": $img");
2281       $suppress_audit = 1;
2282       $body = undef;
2283       return 0;
2284     }
2285
2286     local *OUT;
2287     open (OUT, ">$image_tmp1") || error ("writing $image_tmp1: $!");
2288     print OUT $body || error ("writing $image_tmp1: $!");
2289     close OUT || error ("writing $image_tmp1: $!");
2290
2291   } else {
2292     ($iw, $ih) = image_to_pnm ($img, $body, $image_tmp1);
2293     $body = undef;
2294     if (!$iw || !$ih) {
2295       LOG ($verbose_pbm, "unable to make PBM from $img");
2296       return 0;
2297     }
2298   }
2299
2300   record_success ($load_method, $img, $base);
2301
2302
2303   my $ow = $iw;  # used only for error messages
2304   my $oh = $ih;
2305
2306   # don't just tack this onto the front of the pipeline -- we want it to
2307   # be able to change the size of the input image.
2308   #
2309   if ($filter_cmd) {
2310     LOG ($verbose_pbm, "running $filter_cmd");
2311
2312     my $rc = nontrapping_system "($filter_cmd) < $image_tmp1 >$image_tmp2";
2313     if ($rc != 0) {
2314       LOG(($verbose_pbm || $verbose_load), "failed command: \"$filter_cmd\"");
2315       LOG(($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
2316       return;
2317     }
2318     rename ($image_tmp2, $image_tmp1);
2319
2320     # re-get the width/height in case the filter resized it.
2321     local *IMG;
2322     open(IMG, "<$image_tmp1") || return 0;
2323     $_ = <IMG>;
2324     $_ = <IMG>;
2325     ($iw, $ih) = m/^(\d+) (\d+)$/;
2326     close (IMG);
2327     return 0 unless ($iw && $ih);
2328   }
2329
2330   my $target_w = $img_width;
2331   my $target_h = $img_height;
2332
2333   my $cmd = "";
2334   my $scale = 1.0;
2335
2336
2337   # Usually scale the image to fit on the screen -- but sometimes scale it
2338   # to fit on half or a quarter of the screen.  Note that we don't merely
2339   # scale it to fit, we instead cut it in half until it fits -- that should
2340   # give a wider distribution of sizes.
2341   #
2342   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; $scale /= 2; }
2343   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; $scale /= 2; }
2344
2345   if ($iw > $target_w || $ih > $target_h) {
2346     while ($iw > $target_w ||
2347            $ih > $target_h) {
2348       $iw = int($iw / 2);
2349       $ih = int($ih / 2);
2350     }
2351     if ($iw <= 10 || $ih <= 10) {
2352       LOG ($verbose_pbm, "scaling to ${iw}x$ih would have been bogus.");
2353       return 0;
2354     }
2355
2356     LOG ($verbose_pbm, "scaling to ${iw}x$ih");
2357
2358     $cmd .= " | pnmscale -xsize $iw -ysize $ih";
2359   }
2360
2361
2362   my $src = $image_tmp1;
2363
2364   my $crop_x = 0;     # the sub-rectangle of the image
2365   my $crop_y = 0;     # that we will actually paste.
2366   my $crop_w = $iw;
2367   my $crop_h = $ih;
2368
2369   # The chance that we will randomly crop out a section of an image starts
2370   # out fairly low, but goes up for images that are very large, or images
2371   # that have ratios that make them look like banners (we try to avoid
2372   # banner images entirely, but they slip through when the IMG tags didn't
2373   # have WIDTH and HEIGHT specified.)
2374   #
2375   my $crop_chance = 0.2;
2376   if ($iw > $img_width * 0.4 || $ih > $img_height * 0.4) {
2377     $crop_chance += 0.2;
2378   }
2379   if ($iw > $img_width * 0.7 || $ih > $img_height * 0.7) {
2380     $crop_chance += 0.2;
2381   }
2382   if ($min_ratio && ($iw * $min_ratio) > $ih) {
2383     $crop_chance += 0.7;
2384   }
2385
2386   if ($crop_chance > 0.1) {
2387     LOG ($verbose_pbm, "crop chance: $crop_chance");
2388   }
2389
2390   if (rand() < $crop_chance) {
2391
2392     my $ow = $crop_w;
2393     my $oh = $crop_h;
2394
2395     if ($crop_w > $min_width) {
2396       # if it's a banner, select the width linearly.
2397       # otherwise, select a bell.
2398       my $r = (($min_ratio && ($iw * $min_ratio) > $ih)
2399                ? rand()
2400                : bellrand());
2401       $crop_w = $min_width + int ($r * ($crop_w - $min_width));
2402       $crop_x = int (rand() * ($ow - $crop_w));
2403     }
2404     if ($crop_h > $min_height) {
2405       # height always selects as a bell.
2406       $crop_h = $min_height + int (bellrand() * ($crop_h - $min_height));
2407       $crop_y = int (rand() * ($oh - $crop_h));
2408     }
2409
2410     if ($crop_x != 0   || $crop_y != 0 ||
2411         $crop_w != $iw || $crop_h != $ih) {
2412       LOG ($verbose_pbm,
2413            "randomly cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
2414     }
2415   }
2416
2417   # Where the image should logically land -- this might be negative.
2418   #
2419   my $x = int((rand() * ($img_width  + $crop_w/2)) - $crop_w*3/4);
2420   my $y = int((rand() * ($img_height + $crop_h/2)) - $crop_h*3/4);
2421
2422   # if we have chosen to paste the image outside of the rectangle of the
2423   # screen, then we need to crop it.
2424   #
2425   if ($x < 0 ||
2426       $y < 0 ||
2427       $x + $crop_w > $img_width ||
2428       $y + $crop_h > $img_height) {
2429
2430     LOG ($verbose_pbm,
2431          "cropping for effective paste of ${crop_w}x$crop_h \@ $x,$y");
2432
2433     if ($x < 0) { $crop_x -= $x; $crop_w += $x; $x = 0; }
2434     if ($y < 0) { $crop_y -= $y; $crop_h += $y; $y = 0; }
2435
2436     if ($x + $crop_w >= $img_width)  { $crop_w = $img_width  - $x - 1; }
2437     if ($y + $crop_h >= $img_height) { $crop_h = $img_height - $y - 1; }
2438   }
2439
2440   # If any cropping needs to happen, add pnmcut.
2441   #
2442   if ($crop_x != 0   || $crop_y != 0 ||
2443         $crop_w != $iw || $crop_h != $ih) {
2444     $iw = $crop_w;
2445     $ih = $crop_h;
2446     $cmd .= " | pnmcut $crop_x $crop_y $iw $ih";
2447     LOG ($verbose_pbm, "cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
2448   }
2449
2450   LOG ($verbose_pbm, "pasting ${iw}x$ih \@ $x,$y in $image_ppm");
2451
2452   $cmd .= " | pnmpaste - $x $y $image_ppm";
2453
2454   $cmd =~ s@^ *\| *@@;
2455
2456   if (defined ($webcollage_helper)) {
2457     $cmd = "$webcollage_helper $image_tmp1 $image_ppm " .
2458                               "$scale $opacity " .
2459                               "$crop_x $crop_y $x $y " .
2460                               "$iw $ih";
2461     $_ = $cmd;
2462
2463   } else {
2464     # use a PPM pipeline
2465     $_ = "($cmd)";
2466     $_ .= " < $image_tmp1 > $image_tmp2";
2467   }
2468
2469   if ($verbose_pbm) {
2470     $_ = "($_) 2>&1 | sed s'/^/" . blurb() . "/'";
2471   } else {
2472     $_ .= " 2> /dev/null";
2473   }
2474
2475   my $rc = nontrapping_system ($_);
2476
2477   if (defined ($webcollage_helper) && -z $image_ppm) {
2478     LOG (1, "failed command: \"$cmd\"");
2479     print STDERR "\naudit log:\n\n\n";
2480     print STDERR ("#" x 78) . "\n";
2481     print STDERR blurb() . "$image_ppm has zero size\n";
2482     showlog();
2483     print STDERR "\n\n";
2484     exit (1);
2485   }
2486
2487   if ($rc != 0) {
2488     LOG (($verbose_pbm || $verbose_load), "failed command: \"$cmd\"");
2489     LOG (($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
2490     return;
2491   }
2492
2493   if (!defined ($webcollage_helper)) {
2494     rename ($image_tmp2, $image_ppm) || return;
2495   }
2496
2497   my $target = "$image_ppm";
2498
2499   # don't just tack this onto the end of the pipeline -- we don't want it
2500   # to end up in $image_ppm, because we don't want the results to be
2501   # cumulative.
2502   #
2503   if ($post_filter_cmd) {
2504
2505     my $cmd;
2506
2507     $target = $image_tmp1;
2508     if (!defined ($webcollage_helper)) {
2509       $cmd = "($post_filter_cmd) < $image_ppm > $target";
2510     } else {
2511       # Blah, my scripts need the JPEG data, but some other folks need
2512       # the PPM data -- what to do?  Ignore the problem, that's what!
2513 #     $cmd = "djpeg < $image_ppm | ($post_filter_cmd) > $target";
2514       $cmd = "($post_filter_cmd) < $image_ppm > $target";
2515     }
2516
2517     $rc = nontrapping_system ($cmd);
2518     if ($rc != 0) {
2519       LOG ($verbose_pbm, "filter failed: \"$post_filter_cmd\"\n");
2520       return;
2521     }
2522   }
2523
2524   if (!$no_output_p) {
2525     my $tsize = (stat($target))[7];
2526     if ($tsize > 200) {
2527       $cmd = "$ppm_to_root_window_cmd $target";
2528
2529       # xv seems to hate being killed.  it tends to forget to clean
2530       # up after itself, and leaves windows around and colors allocated.
2531       # I had this same problem with vidwhacker, and I'm not entirely
2532       # sure what I did to fix it.  But, let's try this: launch xv
2533       # in the background, so that killing this process doesn't kill it.
2534       # it will die of its own accord soon enough.  So this means we
2535       # start pumping bits to the root window in parallel with starting
2536       # the next network retrieval, which is probably a better thing
2537       # to do anyway.
2538       #
2539       $cmd .= " &";
2540
2541       $rc = nontrapping_system ($cmd);
2542
2543       if ($rc != 0) {
2544         LOG (($verbose_pbm || $verbose_load), "display failed: \"$cmd\"");
2545         return;
2546       }
2547
2548     } else {
2549       LOG ($verbose_pbm, "$target size is $tsize");
2550     }
2551   }
2552
2553   $source .= "-" . stats_of($source);
2554   print STDOUT "image: ${iw}x${ih} @ $x,$y $base $source\n"
2555     if ($verbose_imgmap);
2556
2557   clearlog();
2558
2559   return 1;
2560 }
2561
2562
2563 sub init_signals {
2564
2565   $SIG{HUP}  = \&signal_cleanup;
2566   $SIG{INT}  = \&signal_cleanup;
2567   $SIG{QUIT} = \&signal_cleanup;
2568   $SIG{ABRT} = \&signal_cleanup;
2569   $SIG{KILL} = \&signal_cleanup;
2570   $SIG{TERM} = \&signal_cleanup;
2571
2572   # Need this so that if giftopnm dies, we don't die.
2573   $SIG{PIPE} = 'IGNORE';
2574 }
2575
2576 END { signal_cleanup(); }
2577
2578
2579 sub main {
2580   $| = 1;
2581   srand(time ^ $$);
2582
2583   my $verbose = 0;
2584   my $dict;
2585   my $driftnet_cmd = 0;
2586
2587   $current_state = "init";
2588   $load_method = "none";
2589
2590   my $root_p = 0;
2591
2592   # historical suckage: the environment variable name is lower case.
2593   $http_proxy = $ENV{http_proxy} || $ENV{HTTP_PROXY};
2594
2595   while ($_ = $ARGV[0]) {
2596     shift @ARGV;
2597     if ($_ eq "-display" ||
2598         $_ eq "-displ" ||
2599         $_ eq "-disp" ||
2600         $_ eq "-dis" ||
2601         $_ eq "-dpy" ||
2602         $_ eq "-d") {
2603       $ENV{DISPLAY} = shift @ARGV;
2604     } elsif ($_ eq "-root") {
2605       $root_p = 1;
2606     } elsif ($_ eq "-no-output") {
2607       $no_output_p = 1;
2608     } elsif ($_ eq "-urls-only") {
2609       $urls_only_p = 1;
2610       $no_output_p = 1;
2611     } elsif ($_ eq "-verbose") {
2612       $verbose++;
2613     } elsif (m/^-v+$/) {
2614       $verbose += length($_)-1;
2615     } elsif ($_ eq "-delay") {
2616       $delay = shift @ARGV;
2617     } elsif ($_ eq "-timeout") {
2618       $http_timeout = shift @ARGV;
2619     } elsif ($_ eq "-filter") {
2620       $filter_cmd = shift @ARGV;
2621     } elsif ($_ eq "-filter2") {
2622       $post_filter_cmd = shift @ARGV;
2623     } elsif ($_ eq "-background" || $_ eq "-bg") {
2624       $background = shift @ARGV;
2625     } elsif ($_ eq "-size") {
2626       $_ = shift @ARGV;
2627       if (m@^(\d+)x(\d+)$@) {
2628         $img_width = $1;
2629         $img_height = $2;
2630       } else {
2631         error "argument to \"-size\" must be of the form \"640x400\"";
2632       }
2633     } elsif ($_ eq "-proxy" || $_ eq "-http-proxy") {
2634       $http_proxy = shift @ARGV;
2635     } elsif ($_ eq "-dictionary" || $_ eq "-dict") {
2636       $dict = shift @ARGV;
2637     } elsif ($_ eq "-driftnet" || $_ eq "--driftnet") {
2638       @search_methods = ( 100, "driftnet", \&pick_from_driftnet );
2639       if (! ($ARGV[0] =~ m/^-/)) {
2640         $driftnet_cmd = shift @ARGV;
2641       } else {
2642         $driftnet_cmd = $default_driftnet_cmd;
2643       }
2644     } elsif ($_ eq "-debug" || $_ eq "--debug") {
2645       my $which = shift @ARGV;
2646       my @rest = @search_methods;
2647       my $ok = 0;
2648       while (@rest) {
2649         my $pct  = shift @rest;
2650         my $name = shift @rest;
2651         my $tfn  = shift @rest;
2652
2653         if ($name eq $which) {
2654           @search_methods = (100, $name, $tfn);
2655           $ok = 1;
2656           last;
2657         }
2658       }
2659       error "no such search method as \"$which\"" unless ($ok);
2660       LOG (1, "DEBUG: using only \"$which\"");
2661
2662     } else {
2663       print STDERR "$copyright\nusage: $progname " .
2664               "[-root] [-display dpy] [-verbose] [-debug which]\n" .
2665         "\t\t  [-timeout secs] [-delay secs] [-filter cmd] [-filter2 cmd]\n" .
2666         "\t\t  [-no-output] [-urls-only] [-background color] [-size WxH]\n" .
2667         "\t\t  [-dictionary dictionary-file] [-http-proxy host[:port]]\n" .
2668         "\t\t  [-driftnet [driftnet-program-and-args]]\n" .
2669         "\n";
2670       exit 1;
2671     }
2672   }
2673
2674   if ($http_proxy && $http_proxy eq "") {
2675     $http_proxy = undef;
2676   }
2677   if ($http_proxy && $http_proxy =~ m@^http://([^/]*)/?$@ ) {
2678     # historical suckage: allow "http://host:port" as well as "host:port".
2679     $http_proxy = $1;
2680   }
2681
2682   if (!$root_p && !$no_output_p) {
2683     print STDERR $copyright;
2684     error "the -root argument is mandatory (for now.)";
2685   }
2686
2687   if (!$no_output_p && !$ENV{DISPLAY}) {
2688     error "\$DISPLAY is not set.";
2689   }
2690
2691
2692   if ($verbose == 1) {
2693     $verbose_imgmap   = 1;
2694     $verbose_warnings = 1;
2695
2696   } elsif ($verbose == 2) {
2697     $verbose_imgmap   = 1;
2698     $verbose_warnings = 1;
2699     $verbose_load     = 1;
2700
2701   } elsif ($verbose == 3) {
2702     $verbose_imgmap   = 1;
2703     $verbose_warnings = 1;
2704     $verbose_load     = 1;
2705     $verbose_filter   = 1;
2706
2707   } elsif ($verbose == 4) {
2708     $verbose_imgmap   = 1;
2709     $verbose_warnings = 1;
2710     $verbose_load     = 1;
2711     $verbose_filter   = 1;
2712     $verbose_net      = 1;
2713
2714   } elsif ($verbose == 5) {
2715     $verbose_imgmap   = 1;
2716     $verbose_warnings = 1;
2717     $verbose_load     = 1;
2718     $verbose_filter   = 1;
2719     $verbose_net      = 1;
2720     $verbose_pbm      = 1;
2721
2722   } elsif ($verbose == 6) {
2723     $verbose_imgmap   = 1;
2724     $verbose_warnings = 1;
2725     $verbose_load     = 1;
2726     $verbose_filter   = 1;
2727     $verbose_net      = 1;
2728     $verbose_pbm      = 1;
2729     $verbose_http     = 1;
2730
2731   } elsif ($verbose >= 7) {
2732     $verbose_imgmap   = 1;
2733     $verbose_warnings = 1;
2734     $verbose_load     = 1;
2735     $verbose_filter   = 1;
2736     $verbose_net      = 1;
2737     $verbose_pbm      = 1;
2738     $verbose_http     = 1;
2739     $verbose_exec     = 1;
2740   }
2741
2742   if ($dict) {
2743     error ("$dict does not exist") unless (-f $dict);
2744     $wordlist = $dict;
2745   } else {
2746     pick_dictionary();
2747   }
2748
2749   init_signals();
2750
2751   spawn_driftnet ($driftnet_cmd) if ($driftnet_cmd);
2752
2753   if ($urls_only_p) {
2754     url_only_output;
2755   } else {
2756     x_or_pbm_output;
2757   }
2758 }
2759
2760 main;
2761 exit (0);