3ab1d816a01651c5850c8d311d394da721bc3c98
[tclrobot.git] / robot.tcl
1 #!/usr/bin/tclsh 
2 # $Id: robot.tcl,v 1.44 2003/06/11 10:11:39 adam Exp $
3 #
4 proc RobotFileNext1 {area lead} {
5     # puts "RobotFileNext1 area=$area lead=$lead"
6     if {[catch {set ns [glob ${area}/*]}]} {
7         return {}
8     }
9     foreach n $ns {
10         if {[file isfile $n]} {
11             set off [string last / $n]
12             # skip /
13             incr off
14             set end [string length $n]
15             # skip _.tkl
16             incr end -6
17             return $lead/[string range $n $off $end]
18         }
19     }
20     foreach n $ns {
21         if {[file isdirectory $n]} {
22             set off [string last / $n]
23             # skip /
24             incr off
25             set sb [RobotFileNext1 $n $lead/[string range $n $off end]]
26             if {[string length $sb]} {
27                 return $sb
28             }
29         }
30     }
31     return {}
32 }
33
34 proc RobotWriteRecord {outf fromurl distance} {
35     puts $outf {<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>}
36     puts $outf "<zmbot>"
37     puts $outf "<distance>"
38     puts $outf $distance
39     puts $outf "</distance>"
40     puts $outf "<fromurl>"
41     puts $outf $fromurl
42     puts $outf "</fromurl>"
43     puts $outf "</zmbot>"
44 }
45
46 proc RobotReadRecord {inf fromurlx distancex} {
47     upvar $fromurlx fromurl
48     upvar $distancex distance
49     gets $inf
50     gets $inf
51     gets $inf
52     set distance [string trim [gets $inf]]
53     # puts "got distance = $distance"
54     gets $inf
55     gets $inf
56     set fromurl [string trim [gets $inf]]
57 }
58
59 proc RobotFileNext {task area} {
60     global control
61     global idletime ns
62     global status
63
64     # puts "RobotFileNext seq=$control($task,seq)"
65     if {$control($task,seq) < 0} {
66         return {}
67     }
68     if {$control($task,seq) == 0} {
69         if {[catch {set ns($task) [glob $task/$area/*]}]} {
70             return done
71         }
72     }
73     # puts "ns=$ns($task)"
74     set off [string length $task/$area]
75     incr off
76     set n [lindex $ns($task) $control($task,seq)]
77     # puts "n=$n"
78     if {![string length $n]} {
79         set control($task,seq) -1
80         flush stdout
81         set statusfile [open $task/status w]
82         puts $statusfile "$status($task,unvisited) $status($task,bad) $status($task,visited)"
83         close $statusfile
84         return wait
85     }
86     incr control($task,seq)
87     if {[file isfile $n/robots.txt_.tkl]} {
88         # puts "ok returning http://[string range $n $off end]/robots.txt"
89         return http://[string range $n $off end]/robots.txt
90     } elseif {[file isdirectory $n]} {
91         set sb [RobotFileNext1 $n http://[string range $n $off end]]
92         if {[string length $sb]} {
93             return $sb
94         }
95     }
96     puts "no more work at end of RobotFileNext n=$n"
97     puts "ns=$ns($task)"
98     return {}
99 }
100
101
102 proc RobotFileExist {task area host path} {
103     global debuglevel
104
105     if {$debuglevel > 3} {
106         puts "RobotFileExist begin area=$area host=$host path=$path"
107     }
108     return [file exists $task/$area/$host${path}_.tkl]
109 }
110
111 proc RobotFileUnlink {task area host path} {
112     global status
113     # puts "RobotFileUnlink begin"
114     # puts "area=$area host=$host path=$path"
115     set npath $task/$area/$host${path}_.tkl
116     # puts "npath=$npath"
117     set comp [split $npath /]
118     if {[catch {exec rm $npath}]} return
119
120     set l [llength $comp]
121     incr l -2
122     incr status($task,$area) -1
123     for {set i $l} {$i > 0} {incr i -1} {
124         set path [join [lrange $comp 0 $i] /]
125         if {![catch {glob $path/*}]} return
126         exec rmdir $path
127     }
128     # puts "RobotFileUnlink end"
129 }
130
131 proc RobotFileClose {out} {
132     if [string compare $out stdout] {
133         close $out
134     }
135 }
136
137 proc RobotFileOpen {task area host path {mode w}} {
138     set orgPwd [pwd]
139     global workdir
140     global status
141     global debuglevel
142
143     # puts "RobotFileOpen task=$task path=$path"
144
145     if {![info exists workdir]} {
146         return stdout
147     }
148     if {$debuglevel > 3} {
149         puts "RobotFileOpen orgPwd=$orgPwd area=$area host=$host path=$path mode=$mode"
150     }
151     if {[string compare $orgPwd $workdir]} {
152         puts "ooops. RobotFileOpen failed"
153         puts "workdir = $workdir"
154         puts "pwd = $orgPwd"
155         exit 1
156     }
157
158     set comp [split $task/$area/$host /]
159     set len [llength $comp]
160     incr len -1
161
162     # puts "1 comp=$comp"
163
164     for {set i 0} {$i <= $len} {incr i} {
165         set d [lindex $comp $i]
166         if {[string length $d] == 0} {
167             cd /
168         } elseif {[catch {cd $d}]} {
169             exec mkdir $d
170             cd ./$d
171             if {![string compare $area unvisited] && $i == $len && $mode == "w"} {
172                 if {[string compare $path /robots.txt]} {
173                     set out [open robots.txt_.tkl w]
174                     puts "creating robots.txt in $d"
175                     close $out
176                     incr status($task,unvisited)
177                 }
178             }
179         }
180     }
181
182     set comp [split $path /]
183     set len [llength $comp]
184     incr len -1
185
186     # puts "2 path=$path comp=$comp"
187
188     for {set i 0} {$i < $len} {incr i} {
189         set d [lindex $comp $i]
190         if {[string length $d] > 0} {
191             if {[catch {cd $d}]} {
192                 exec mkdir $d
193                 cd ./$d
194             }
195         }
196     }
197     set d [lindex $comp $len]
198     set out [open ${d}_.tkl $mode]
199     if {$mode == "w"} {
200         incr status($task,$area)
201     }
202     cd $orgPwd
203     return $out
204 }
205
206 proc RobotStartJob {fname t} {
207     global control
208
209     set f [open $fname r]
210     set xml [read $f]
211     puts "Reading $fname"
212     close $f
213     if {![regexp {<status>([^<]*)</status>} $xml x status]} {
214         return
215     }
216     if {$status == "done"} {
217         puts "already done"
218         return
219     }
220     puts "status = $status"
221     if {![task $t]} {
222         return
223     }
224     htmlSwitch $xml \
225         url {
226             url $body
227         } filter {
228             set type $parm(type)
229             set action $parm(action)
230             if {$type == "domain"} {
231                 $action url http://$body/*
232             }
233             if {$type == "url"} {
234                 $action url $body
235             }
236             if {$type == "mime"} {
237                 $action mime $body
238             }
239         } distance {
240             set control($t,distance) $body
241         } status {
242             set control($t,filestatus) $body
243         }
244     if {$status == "pending"} {
245         regsub {<status>[^<]*</status>} $xml {<status>running</status>} xml2
246         set f [open $fname w]
247         puts -nonewline $f $xml2 
248         close $f
249     }
250 }
251
252 proc RobotDoneJob {t} {
253     global daemon_dir
254
255     if {![info exists daemon_dir]} {
256         return
257     }
258
259     set fname $t.tkl
260
261     set f [open $fname r]
262     set xml [read $f]
263     puts "Reading $fname"
264     regexp {<status>([^<]*)</status>} $xml x status
265     puts "------"
266     puts "status = $status"
267     close $f
268
269     regsub {<status>[^<]*</status>} $xml {<status>done</status>} xml2
270     set f [open $fname w]
271     puts -nonewline $f $xml2 
272     close $f
273 }
274
275 proc RobotScanDir {} {
276     global daemon_dir
277
278     if {![info exists daemon_dir]} {
279         return
280     }
281     foreach d $daemon_dir {
282         if {[catch {set files [glob $d/*.tkl]}]} {
283             return
284         }
285         foreach fname $files {
286             if {[file isfile $fname] && [file readable $fname]} {
287                 set t [file rootname $fname]
288                 RobotStartJob $fname $t
289             }
290         }
291     }
292 }
293
294 proc RobotRR {task} {
295     global control robotsRunning tasks robotsMax status
296
297     puts "RobotRR -- running=$robotsRunning max=$robotsMax---------------"
298     incr robotsRunning -1
299
300     # only one task gets through...
301     if {[string compare [lindex $tasks 0] $task]} {
302         return
303     }
304     puts "RobotRR. task = $task"
305     while {$robotsRunning} {
306         vwait robotsRunning
307     }
308     puts "Scan"
309     if {[catch {RobotScanDir} msg]} {
310         puts "RobotScanDir failed"
311         puts $msg
312     }
313     foreach t $tasks {
314         set statusfile [open $t/status w]
315         puts $statusfile "$status($t,unvisited) $status($t,bad) $status($t,visited)"
316         close $statusfile
317         set control($t,seq) 0
318         RobotStart $t
319     }
320 }
321
322 proc RobotDaemonSig {} {
323     global daemon_cnt
324
325     incr daemon_cnt
326 }
327
328 proc RobotDaemonLoop {} {
329     global daemon_cnt tasks robotsRunning status
330
331     set daemon_cnt 0
332     while 1 {
333         puts $daemon_cnt
334         
335         RobotScanDir
336         
337         if {[info exists tasks]} {
338             puts "daemon loop tasks $tasks"
339             foreach t $tasks {
340                 set control($t,seq) 0
341                 RobotStart $t
342             }
343             while {$robotsRunning} {
344                 vwait robotsRunning
345             }
346         }
347         after 30000 RobotDaemonSig
348         vwait daemon_cnt
349     }
350 }
351
352 proc RobotRestart {task url sock} {
353     global URL robotsRunning
354
355     close $sock
356     after cancel $URL($sock,cancel) 
357
358     foreach v [array names URL $task,$url,*] {
359         unset URL($v)
360     }
361
362     incr robotsRunning -1
363     RobotStart $task
364 }
365
366 proc RobotStart {task} {
367     global URL
368     global robotsRunning robotsMax idletime status tasks
369   
370     # puts "RobotStart $task running=$robotsRunning"
371     while {1} {
372         set url [RobotFileNext $task unvisited]
373         if {[string compare $url done] == 0} {
374             puts "In RobotStart task $task done"
375
376             catch {unset ntasks}
377             foreach t $tasks {
378                 if {[string compare $t $task]} {
379                     lappend ntasks $t
380                 } else {
381                     puts "task $t done"
382                 }
383             }
384             if {![info exists ntasks]} {
385                 unset tasks
386                 puts "all done"
387             } else {
388                 set tasks $ntasks
389             }
390             RobotDoneJob $task
391             return
392         }
393         if {![string length $url]} {
394             return
395         }
396         incr robotsRunning
397         if {[string compare $url wait] == 0} {
398             after $idletime [list RobotRR $task]
399             return
400         }
401         set r [RobotGetUrl $task $url {}]
402         if {!$r} {
403             if {$robotsRunning >= $robotsMax} return
404         } else {
405             incr robotsRunning -1
406             if {![RobotFileExist $task bad $URL($task,$url,hostport) $URL($task,$url,path)]} {
407                 set outf [RobotFileOpen $task bad $URL($task,$url,hostport) $URL($task,$url,path)]
408                 RobotFileClose $outf
409             }
410             RobotFileUnlink $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)
411         }
412     }
413 }
414
415 proc headSave {task url out} {
416     global URL
417     
418     if {[info exists URL($task,$url,head,last-modified)]} {
419         puts $out "<lastmodified>$URL($task,$url,head,last-modified)</lastmodified>"
420     }
421     puts $out {<si>}
422     if {[info exists URL($task,$url,head,date)]} {
423         puts $out " <date>$URL($task,$url,head,date)</date>"
424     }
425     if {[info exists URL($task,$url,head,content-length)]} {
426         puts $out " <by>$URL($task,$url,head,content-length)</by>"
427     }
428     if {[info exists URL($task,$url,head,server)]} {
429         puts $out " <format>$URL($task,$url,head,server)</format>"
430     }
431     puts $out {</si>}
432     puts $out {<publisher>}
433     puts $out " <identifier>$url</identifier>"
434     if {[info exists URL($task,$url,head,content-type)]} {
435         puts $out " <type>$URL($task,$url,head,content-type)</type>"
436     }
437     puts $out {</publisher>}
438 }
439
440 proc RobotHref {task url hrefx hostx pathx} {
441     global URL control debuglevel
442     upvar $hrefx href
443     upvar $hostx host
444     upvar $pathx path
445
446     if {$debuglevel > 1} {
447         puts "Ref input url = $url href=$href"
448     }
449
450     if {[string first { } $href] >= 0} {
451         return 0
452     }
453     if {[string length $href] > 256} {
454         return 0
455     }
456
457 #   Skip pages that have ? in them
458 #    if {[string first {?} $url] >= 0 && [string first {?} $href] >= 0} {
459 #       return 0
460 #    }
461     # get method (if any)
462     if {![regexp {^([^/:]+):(.*)} $href x method hpath]} {
463         set hpath $href
464         set method http
465     } else {
466         if {[string compare $method http]} {
467             return 0
468         }
469     }
470     # get host (if any)
471     if {[regexp {^//([^/]+)([^\#]*)} $hpath x host surl]} {
472         if {![string length $surl]} {
473             set surl /
474         }
475         if {[info exist control($task,domains)]} {
476             set ok 0
477             foreach domain $control($task,domains) {
478                 if {[string match $domain $host]} {
479                     set ok 1
480                     break
481                 }
482             }
483             if {!$ok} {
484                 return 0
485             }
486         }
487     } else {
488         regexp {^([^\#]*)} $hpath x surl
489         set host $URL($task,$url,hostport)
490     }
491     if {![string length $surl]} {
492         return 0
493     }
494     if {[string first / $surl]} {
495         # relative path
496         set curpath $URL($task,$url,path)
497         if {[info exists URL($task,$url,bpath)]} {
498             set curpath $URL($task,$url,bpath)
499         }
500         regexp {^([^\#?]*)} $curpath x dpart
501         set l [string last / $dpart]
502         if {[expr $l >= 0]} {
503             set surl [string range $dpart 0 $l]$surl
504         } else {
505             set surl $dpart/$surl
506         }
507     }
508     set surllist [split $surl /]
509     catch {unset path}
510     set pathl 0
511     foreach c $surllist {
512         switch -- $c {
513             .. {
514                 if {$pathl > 1} {
515                     incr pathl -2
516                     set path [lrange $path 0 $pathl]
517                     incr pathl
518                 }
519             }
520             . {
521
522             }
523             default {
524                 incr pathl
525                 lappend path $c
526             }
527         }
528     }
529     if {$debuglevel > 4} {
530         puts "pathl=$pathl output path=$path"
531     }
532     set path [join $path /]
533     if {![string length $path]} {
534         set path /
535     }
536     regsub -all {~} $path {%7E} path
537     set href "$method://$host$path"
538
539     if {$debuglevel > 1} {
540         puts "Ref result = $href"
541     }
542     return [checkrule $task url $href]
543 }
544
545 proc RobotError {task url code} {
546     global URL
547
548     puts "Bad URL $url (code $code)"
549     set fromurl {}
550     set distance -1
551     if {[RobotFileExist $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)]} {
552         set inf [RobotFileOpen $task unvisited $URL($task,$url,hostport) $URL($task,$url,path) r]
553         RobotReadRecord $inf fromurl distance
554         RobotFileClose $inf
555     }
556     RobotFileUnlink $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)
557     if {![RobotFileExist $task bad $URL($task,$url,hostport) $URL($task,$url,path)]} {
558         set outf [RobotFileOpen $task bad $URL($task,$url,hostport) $URL($task,$url,path)]
559         RobotWriteRecord $outf $fromurl $distance
560         RobotFileClose $outf
561     }
562 }
563
564 proc RobotRedirect {task url tourl code} {
565     global URL
566
567     puts "Redirecting from $url to $tourl"
568
569     set distance {}
570     set fromurl {}
571     if {[RobotFileExist $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)]} {
572         set inf [RobotFileOpen $task unvisited $URL($task,$url,hostport) $URL($task,$url,path) r]
573         RobotReadRecord $inf fromurl distance
574         RobotFileClose $inf
575     }
576     if {![RobotFileExist $task bad $URL($task,$url,hostport) $URL($task,$url,path)]} {
577         set outf [RobotFileOpen $task bad $URL($task,$url,hostport) $URL($task,$url,path)]
578         RobotWriteRecord $outf $fromurl $distance
579         RobotFileClose $outf
580     }
581     if {[RobotHref $task $url tourl host path]} {
582         if {![RobotFileExist $task visited $host $path]} {
583             if {![RobotFileExist $task unvisited $host $path]} {
584                 set outf [RobotFileOpen $task unvisited $host $path]
585                 RobotWriteRecord $outf $fromurl $distance
586                 RobotFileClose $outf
587             }
588         } else {
589             set olddistance {}
590             set inf [RobotFileOpen $task visited $host $path r]
591             RobotReadRecord $inf oldurl olddistance
592             RobotFileClose $inf
593             if {[string length $olddistance] == 0} {
594                 set olddistance 1000
595             }
596             if {[string length $distance] == 0} {
597                 set distance 1000
598             }
599             puts "distance=$distance olddistance=$olddistance"
600             if {[expr $distance < $olddistance]} {
601                 set outf [RobotFileOpen $task unvisited $host $path]
602                 RobotWriteRecord $outf $tourl $distance
603                 RobotFileClose $outf
604             }
605         }
606     }
607     if {[catch {RobotFileUnlink $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)}]} {
608         puts "unlink failed"
609         exit 1
610     }
611 }
612
613 proc link {task url out href body distance} {
614     global URL control
615     if {[expr $distance > $control($task,distance)]} return
616     
617     if {![RobotHref $task $url href host path]} return
618     
619     puts $out "<cr>"
620     puts $out "<identifier>$href</identifier>"
621     puts $out "<description>$body</description>"
622     puts $out "</cr>"
623     
624     if {![RobotFileExist $task visited $host $path]} {
625         set olddistance 1000
626         if {![RobotFileExist $task bad $host $path]} {
627             if {[RobotFileExist $task unvisited $host $path]} {
628                 set inf [RobotFileOpen $task unvisited $host $path r]
629                 RobotReadRecord $inf oldurl olddistance
630                 RobotFileClose $inf
631             }
632         } else {
633             set olddistance 0
634         }
635         if {[string length $olddistance] == 0} {
636             set olddistance 1000
637         }
638         if {[expr $distance < $olddistance]} {
639             set outf [RobotFileOpen $task unvisited $host $path]
640             RobotWriteRecord $outf $url $distance
641             RobotFileClose $outf
642         }
643     } elseif {[string compare $href $url]} {
644         set inf [RobotFileOpen $task visited $host $path r]
645         RobotReadRecord $inf xurl olddistance
646         close $inf
647         if {[string length $olddistance] == 0} {
648             set olddistance 1000
649         }
650         if {[expr $distance < $olddistance]} {
651             puts "OK remarking url=$url href=$href"
652             puts "olddistance = $olddistance"
653             puts "newdistance = $distance"
654             set outf [RobotFileOpen $task unvisited $host $path]
655             RobotWriteRecord $outf $url $distance
656             RobotFileClose $outf
657         }
658     }
659 }
660
661 proc RobotTextHtml {task url out} {
662     global URL control
663
664     # set title so we can emit it for the body
665     set title {}
666     # if true, nothing will be indexed
667     set noindex 0
668     # if true, nothing will be followed
669     set nofollow 0
670
671     set distance 0
672     set fdistance 0
673     if {$control($task,distance) < 1000 && [info exists URL($task,$url,dist)]} {
674         set fdistance $URL($task,$url,dist)
675         set distance [expr $fdistance + 1]
676     }
677     htmlSwitch $URL($task,$url,buf) \
678         title {
679             set title $body
680         } -nonest meta {
681             # collect metadata and save NAME= CONTENT=..
682             set metaname {}
683             set metacontent {}
684             puts -nonewline $out "<meta"
685             set al [array names parm]
686             foreach a $al {
687                 set al [string tolower $a]
688                 puts -nonewline $out " $al"
689                 puts -nonewline $out {="}
690                 puts -nonewline $out $parm($a)
691                 puts -nonewline $out {"}
692                 switch -- $al {
693                     "name" {
694                         set metaname [string tolower $parm($a)]
695                     }
696                     "content" {
697                         set metacontent $parm($a)
698                     }
699                 }
700                 unset parm($al)
701             }
702             puts $out "></meta>"
703             # go through robots directives (af any)
704             if {![string compare $metaname robots]} {
705                 set direcs [split [string tolower $metacontent] ,]
706                 if {[lsearch $direcs noindex] >= 0} {
707                     set noindex 1
708                 }
709                 if {[lsearch $direcs nofollow] >= 0} {
710                     set nofollow 1
711                 }
712             }
713         } body {
714             # don't print title of document content if noindex is used
715             if {!$noindex} {
716                 puts $out "<title>$title</title>"
717                 regsub -all {<!--[^-]*-->} $body { } abody
718                 regsub -all -nocase {<script[^<]*</script>} $abody {} bbody
719                 regsub -all {<[^\>]+>} $bbody {} nbody
720                 puts $out "<documentcontent>"
721                 puts $out $nbody
722                 puts $out "</documentcontent>"
723             }
724         } -nonest base {
725             # <base href=.. >
726             if {![info exists parm(href)]} {
727                 continue
728             }
729             set href [string trim $parm(href)]
730             if {![RobotHref $task $url href host path]} continue
731             set URL($task,$url,bpath) $path
732         } a {
733             # <a href="...."> .. </a> 
734             # we're not using nonest - otherwise body isn't set
735             if {$nofollow} continue
736             if {![info exists parm(href)]} {
737                 continue
738             }
739             link $task $url $out [string trim $parm(href)] $body $distance
740         } -nonest area {
741             if {$nofollow} continue
742             if {![info exists parm(href)]} {
743                 continue
744             }
745             link $task $url $out [string trim $parm(href)] $body $distance
746         } -nonest frame {
747             if {![info exists parm(src)]} {
748                 continue
749             }
750             link $task $url $out [string trim $parm(src)] $body $fdistance
751         }
752 }
753
754 proc RobotsTxt {task url} {
755     global agent URL
756
757     RobotsTxt0 $task URL(URL($task,$url,hostport),robots) $URL($task,$url,buf)
758 }
759
760 proc RobotsTxt0 {task v buf} {
761     global URL agent
762     set section 0
763     foreach l [split $buf \n] {
764         if {[regexp {([-A-Za-z]+):[ ]*([^\# ]+)} $l match cmd arg]} {
765             set arg [string trim $arg]
766             puts "cmd=$cmd arg=$arg"
767             switch -- [string tolower $cmd] {
768                 user-agent {
769                     if {$section} break
770                     set pat [string tolower $arg]*
771                     set section [string match $pat $agent]
772                 }
773                 disallow {
774                     if {$section} {
775                         puts "rule [list 0 $arg]"
776                         lappend $v [list 0 $arg]
777                     }
778                 }
779                 allow {
780                     if {$section} {
781                         puts "rule [list 1 $arg]"
782                         lappend $v [list 1 $arg]
783                     }
784                 }
785             }
786         }
787     }
788 }
789
790 proc RobotTextPlain {task url out} {
791     global URL
792
793     puts $out "<documentcontent>"
794     regsub -all {<} $URL($task,$url,buf) {\&lt;} content
795     puts $out $content
796     puts $out "</documentcontent>"
797
798     if {![string compare $URL($task,$url,path) /robots.txt]} {
799         RobotsTxt $task $url
800     }
801 }
802
803 proc RobotWriteMetadata {task url out} {
804     global URL
805
806     set charset $URL($task,$url,charset)
807     puts $out "<?xml version=\"1.0\" encoding=\"$charset\" standalone=\"yes\"?>"
808     puts $out "<zmbot>"
809
810     set distance 1000
811     if {[RobotFileExist $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)]} {
812         set inf [RobotFileOpen $task unvisited $URL($task,$url,hostport) $URL($task,$url,path) r]
813         RobotReadRecord $inf fromurl distance
814         RobotFileClose $inf
815     }
816     set URL($task,$url,dist) $distance
817     puts $out "<distance>"
818     puts $out "  $distance"
819     puts $out "</distance>"
820     headSave $task $url $out
821     puts "Parsing $url distance=$distance"
822     switch $URL($task,$url,head,content-type) {
823         text/html {
824             if {[string length $distance]} {
825                 RobotTextHtml $task $url $out
826             }
827         }
828         text/plain {
829             RobotTextPlain $task $url $out
830         }
831     }
832     puts $out "</zmbot>"
833 }
834
835 proc Robot200 {task url} {
836     global URL
837     
838     set out [RobotFileOpen $task raw $URL($task,$url,hostport) $URL($task,$url,path)]
839     puts -nonewline $out $URL($task,$url,buf)
840     RobotFileClose $out
841
842     set out [RobotFileOpen $task visited $URL($task,$url,hostport) $URL($task,$url,path)]
843     RobotWriteMetadata $task $url $out
844     RobotFileClose $out
845
846     RobotFileUnlink $task unvisited $URL($task,$url,hostport) $URL($task,$url,path)
847 }
848
849 proc RobotReadContent {task url sock binary} {
850     global URL
851
852     set buffer [read $sock 16384]
853     set readCount [string length $buffer]
854
855     if {$readCount <= 0} {
856         Robot200 $task $url
857         RobotRestart $task $url $sock
858     } elseif {!$binary && [string first \0 $buffer] >= 0} {
859         Robot200 $task $url
860         RobotRestart $task $url $sock
861     } else {
862         # puts "Got $readCount bytes"
863         set URL($task,$url,buf) $URL($task,$url,buf)$buffer
864     }
865 }
866
867 proc RobotReadHeader {task url sock} {
868     global URL debuglevel
869
870     if {$debuglevel > 1} {
871         puts "HTTP head $url"
872     }
873     if {[catch {set buffer [read $sock 2148]}]} {
874         RobotError $task $url 404
875         RobotRestart $task $url $sock
876         return
877     }
878     set readCount [string length $buffer]
879     
880     if {$readCount <= 0} {
881         RobotError $task $url 404
882         RobotRestart $task $url $sock
883     } else {
884         # puts "Got $readCount bytes"
885         set URL($task,$url,buf) $URL($task,$url,buf)$buffer
886         
887         set n [string first \r\n\r\n $URL($task,$url,buf)]
888         if {$n > 1} {
889             set code 0
890             set version {}
891             set headbuf [string range $URL($task,$url,buf) 0 $n]
892             incr n 4
893             set URL($task,$url,charset) ISO-8859-1
894             set URL($task,$url,buf) [string range $URL($task,$url,buf) $n end]
895             
896             regexp {^HTTP/([0-9.]+)[ ]+([0-9]+)} $headbuf x version code
897             set lines [split $headbuf \n]
898             foreach line $lines {
899                 if {[regexp {^([^:]+):[ ]+([^;]*)} $line x name value]} {
900                     set URL($task,$url,head,[string tolower $name]) [string trim $value]
901                 }
902                 regexp {^Content-Type:.*charset=([A-Za-z0-9_-]*)} $line x URL($task,$url,charset)
903             }
904             puts "HTTP CODE $code"
905             set URL($task,$url,state) skip
906             switch $code {
907                 301 {
908                     RobotRedirect $task $url $URL($task,$url,head,location) 301
909                     RobotRestart $task $url $sock
910                 }
911                 302 {
912                     RobotRedirect $task $url $URL($task,$url,head,location) 302
913                     RobotRestart $task $url $sock
914                 }
915                 200 {
916                     if {![info exists URL($task,$url,head,content-type)]} {
917                         set URL($task,$url,head,content-type) {}
918                     }
919                     set binary 1
920                     switch -glob -- $URL($task,$url,head,content-type) {
921                         text/* {
922                             set binary 0
923                         }
924                     }
925                     if {![regexp {/robots.txt$} $url]} {
926                         if {![checkrule $task mime $URL($task,$url,head,content-type)]} {
927                             RobotError $task $url mimedeny
928                             RobotRestart $task $url $sock
929                             return
930                         }
931                     }
932                     fileevent $sock readable [list RobotReadContent $task $url $sock $binary]
933                 }
934                 default {
935                     RobotError $task $url $code
936                     RobotRestart $task $url $sock
937                 }
938             }
939         }
940     }
941 }
942
943 proc RobotSockCancel {task url sock} {
944
945     puts "RobotSockCancel sock=$sock url=$url"
946     RobotError $task $url 401
947     RobotRestart $task $url $sock
948 }
949
950 proc RobotConnect {task url sock} {
951     global URL agent acceptLanguage
952
953     fconfigure $sock -translation {lf crlf} -blocking 0
954     fileevent $sock readable [list RobotReadHeader $task $url $sock]
955     puts $sock "GET $URL($task,$url,path) HTTP/1.0"
956     puts $sock "Host: $URL($task,$url,host)"
957     puts $sock "User-Agent: $agent"
958     if {[string length $acceptLanguage]} {
959         puts $sock "Accept-Language: $acceptLanguage"
960     }
961     puts $sock ""
962     set URL($sock,cancel) [after 30000 [list RobotSockCancel $task $url $sock]]
963     if {[catch {flush $sock}]} {
964         RobotError $task $url 404
965         RobotRestart $task $url $sock
966     }
967 }
968
969 proc RobotNop {} {
970
971 }
972
973 proc RobotGetUrl {task url phost} {
974     global URL robotsRunning
975     flush stdout
976     puts "Retrieve running=$robotsRunning url=$url task=$task"
977     if {![regexp {([^:]+)://([^/]+)(.*)} $url x method hostport path]} {
978         return -1
979     }
980     if {![regexp {([^:]+):([0-9]+)} $hostport x host port]} {
981         set port 80
982         set host $hostport
983     }
984     set URL($task,$url,method) $method
985     set URL($task,$url,host) $host
986     set URL($task,$url,hostport) $hostport
987     set URL($task,$url,path) $path
988     set URL($task,$url,state) head
989     set URL($task,$url,buf) {}
990
991     if {[string compare $path /robots.txt]} {
992         set ok 1
993         if {![info exists URL($hostport,robots)]} {
994             puts "READING robots.txt for host $hostport"
995             if {[RobotFileExist $task visited $hostport /robots.txt]} {
996                 set inf [RobotFileOpen $task visited $hostport /robots.txt r]
997                 set buf [read $inf 32768]
998                 close $inf
999             } else {
1000                 set buf "User-agent: *\nAllow: /\n"
1001             }
1002             RobotsTxt0 $task URL($hostport,robots) $buf
1003         }
1004         if {[info exists URL($hostport,robots)]} {
1005             foreach l $URL($hostport,robots) {
1006                 if {[string first [lindex $l 1] $path] == 0} {
1007                     set ok [lindex $l 0]
1008                     break
1009                 }
1010             }
1011         }
1012         if {!$ok} {
1013             puts "skipped due to robots.txt"
1014             return -1
1015         }
1016     }
1017     if [catch {set sock [socket -async $host $port]}] {
1018         return -1
1019     }
1020     RobotConnect $task $url $sock
1021
1022     return 0
1023 }
1024
1025 if {![llength [info commands htmlSwitch]]} {
1026     set e [info sharedlibextension]
1027     if {[catch {load ./tclrobot$e}]} {
1028         load tclrobot$e
1029     }
1030 }
1031
1032 set agent "zmbot/0.2"
1033 if {![catch {set os [exec uname -s -r]}]} {
1034     set agent "$agent ($os)"
1035 }
1036
1037 puts "agent: $agent"
1038
1039 proc bgerror {m} {
1040     global errorInfo
1041     puts "BGERROR $m"
1042     puts $errorInfo
1043 }
1044
1045 set robotsRunning 0
1046 set workdir [pwd]
1047 set idletime 30000
1048 set acceptLanguage {}
1049 set debuglevel 0
1050
1051 # Rules: allow, deny, url
1052
1053 proc checkrule {task type this} {
1054     global control
1055     global debuglevel
1056
1057     set default_ret 1
1058
1059     if {$debuglevel > 3} {
1060         puts "CHECKRULE $type $this"
1061     }
1062     if {[info exist control($task,alrules)]} {
1063         foreach l $control($task,alrules) {
1064             if {$debuglevel > 3} {
1065                 puts "consider $l"
1066             }
1067             # consider type
1068             if {[lindex $l 1] != $type} continue
1069             # consider mask (! negates)
1070             set masks [lindex $l 2]
1071             set ok 0
1072             set default_ret 0
1073             foreach mask $masks {       
1074                 if {$debuglevel > 4} {
1075                     puts "consider single mask $mask"
1076                 }
1077                 if {[string index $mask 0] == "!"} {
1078                     set mask [string range $mask 1 end]
1079                     if {[string match $mask $this]}  continue
1080                 } else {
1081                     if {![string match $mask $this]} continue
1082                 }
1083                 set ok 1
1084             }
1085             if {$debuglevel > 4} {
1086                 puts "ok = $ok"
1087             }
1088             if {!$ok} continue
1089             # OK, we have a match
1090             if {[lindex $l 0] == "allow"} {
1091                 if {$debuglevel > 3} {
1092                     puts "CHECKRULE MATCH OK"
1093                 }
1094                 return 1
1095             } else {
1096                 if {$debuglevel > 3} {
1097                     puts "CHECKFULE MATCH FAIL"
1098                 }
1099                 return 0
1100             }
1101         }
1102     }
1103     if {$debuglevel > 3} {
1104         puts "CHECKRULE MATCH DEFAULT $default_ret"
1105     }
1106     return $default_ret
1107 }
1108
1109
1110 proc url {href} {
1111     global debuglevel task
1112
1113     if {[RobotHref $task http://www.indexdata.dk/ href host path]} {
1114         if {![RobotFileExist $task visited $host $path]} {
1115             set outf [RobotFileOpen $task unvisited $host $path]
1116             RobotWriteRecord $outf href 0
1117             RobotFileClose $outf
1118         }
1119     }
1120 }
1121
1122 proc deny {type stuff} {
1123     global control task
1124
1125     lappend control($task,alrules) [list deny $type $stuff]
1126 }
1127
1128 proc allow {type stuff} {
1129     global control task
1130
1131     lappend control($task,alrules) [list allow $type $stuff]
1132 }
1133
1134 proc debug {level} {
1135     global debuglevel
1136
1137     set debuglevel $level
1138 }
1139
1140 proc task {t} {
1141     global tasks task status control
1142
1143     set task $t
1144
1145     if {[info exists tasks]} {
1146         if {[lsearch -exact $tasks $t] >= 0} {
1147             return 0
1148         }
1149     }
1150
1151     lappend tasks $t
1152     set status($t,unvisited) 0
1153     set status($t,visited) 0
1154     set status($t,bad) 0
1155     set status($t,raw) 0
1156     set status($t,active) 1
1157     set control($t,seq) 0
1158     set control($t,distance) 10
1159     return 1
1160 }
1161
1162 # Little utility that ensures that at least one task is present (main).
1163 proc chktask {} {
1164     global tasks
1165     if {![info exist tasks]} {
1166         task main
1167     }
1168 }
1169
1170
1171 # Parse options
1172
1173 set i 0
1174 set l [llength $argv]
1175
1176 if {$l < 2} {
1177     puts {tclrobot: usage:}
1178     puts {tclrobot [-j jobs] [-i idle] [-c count] [-d domain] [-D dir] [-r rules] [url ..]}
1179     puts " Example: -c 3 -d '*.dk' http://www.indexdata.dk/"
1180
1181     exit 1
1182 }
1183
1184
1185 while  {$i < $l} {
1186     set arg [lindex $argv $i]
1187     switch -glob -- $arg {
1188         -t* {
1189             set t [string range $arg 2 end]
1190             if {![string length $t]} {
1191                 set t [lindex $argv [incr i]]
1192             }
1193             task $t
1194         }
1195         -D* {
1196             set dir [string range $arg 2 end]
1197             if {![string length $dir]} {
1198                 set dir [lindex $argv [incr i]]
1199             }
1200             lappend daemon_dir $dir
1201         }
1202         -j* {
1203             set robotsMax [string range $arg 2 end]
1204             if {![string length $robotsMax]} {
1205                 set robotsMax [lindex $argv [incr i]]
1206             }
1207         }
1208         -c* {
1209             chktask
1210             set control($task,distance) [string range $arg 2 end]
1211             if {![string length $control($task,distance)]} {
1212                 set control($task,distance) [lindex $argv [incr i]]
1213             }
1214         }
1215         -d* {
1216             chktask
1217             set dom [string range $arg 2 end]
1218             if {![string length $dom]} {
1219                 set dom [lindex $argv [incr i]]
1220             }
1221             lappend control($task,domains) $dom
1222         }
1223         -i* {
1224             set idletime [string range $arg 2 end]
1225             if {![string length $idletime]} {
1226                 set idletime [lindex $argv [incr i]]
1227             }
1228         }
1229         -l* {
1230             chktask
1231             set acceptLanguage [string range $arg 2 end]
1232             if {![string length $acceptLanguage]} {
1233                 set acceptLanguage [lindex $argv [incr i]]
1234             }
1235         }
1236         -r* {
1237             chktask
1238             set rfile [string range $arg 2 end]
1239             if {![string length $rfile]} {
1240                 set rfile [lindex $argv [incr i]]
1241             }
1242             catch {unset maxdistance}
1243             source $rfile
1244             if {[info exists maxdistance]} {
1245                 set control($task,distance) $maxdistance
1246             }
1247         }
1248         default {
1249             chktask
1250             set href $arg
1251             if {[RobotHref $task http://www.indexdata.dk/ href host path]} {
1252                 if {![RobotFileExist $task visited $host $path]} {
1253                     set outf [RobotFileOpen $task unvisited $host $path]
1254                     RobotWriteRecord $outf href 0
1255                     RobotFileClose $outf
1256                 }
1257             }
1258         }
1259     }
1260     incr i
1261 }
1262
1263 if {![info exist robotsMax]} {
1264     set robotsMax 5
1265 }
1266
1267 if {[info exist daemon_dir]} {
1268     RobotDaemonLoop
1269 } else {
1270     foreach t $tasks {
1271         puts "task $t"
1272         puts "max distance=$control($t,distance)"
1273         if {[info exists control($t,domains)]} {
1274             puts "domains=$control($t,domains)"
1275         }
1276     }
1277     puts "max jobs=$robotsMax"
1278     
1279     foreach t $tasks {
1280         RobotStart $t
1281     }
1282     
1283     while {$robotsRunning} {
1284         vwait robotsRunning
1285     }
1286     
1287     if {[info exists tasks]} {
1288         foreach t $tasks {
1289             set statusfile [open $t/status w]
1290             puts $statusfile "$status($t,unvisited) $status($t,bad) $status($t,visited)"
1291             close $statusfile
1292         }
1293     }
1294 }
1295