]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/URLVerify.pm
cbda9449e208e6371d9638e4b9fcde298d3b0ba8
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / URLVerify.pm
1 package OpenILS::Application::URLVerify;
2
3 # For code searchability, I'm telling you this is the "link checker."
4
5 use base qw/OpenILS::Application/;
6 use strict; use warnings;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenSRF::MultiSession;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenILS::Utils::CStoreEditor q/:funcs/;
12 use OpenILS::Application::AppUtils;
13 use LWP::UserAgent;
14
15 use Data::Dumper;
16
17 $Data::Dumper::Indent = 0;
18
19 my $U = 'OpenILS::Application::AppUtils';
20
21 my $user_agent_string;
22
23 sub initialize {
24     my $conf = new OpenSRF::Utils::SettingsClient;
25
26     my @confpath = qw/apps open-ils.url_verify app_settings user_agent/;
27
28     $user_agent_string =
29         sprintf($conf->config_value(@confpath), __PACKAGE__->ils_version);
30
31     $logger->info("using '$user_agent_string' as User Agent string");
32 }
33
34 __PACKAGE__->register_method(
35     method => 'verify_session',
36     api_name => 'open-ils.url_verify.session.verify',
37     stream => 1,
38     max_chunk_size => 0,
39     signature => {
40         desc => q/
41             Performs verification on all (or a subset of the) URLs within the requested session.
42         /,
43         params => [
44             {desc => 'Authentication token', type => 'string'},
45             {desc => 'Session ID (url_verify.session.id)', type => 'number'},
46             {desc => 'URL ID list (optional).  An empty list will result in no URLs being processed, but null will result in all the URLs for the session being processed', type => 'array'},
47             {
48                 desc => q/
49                     Options (optional).
50                         report_all => bypass response throttling and return all URL sub-process
51                             responses to the caller.  Not recommened for remote (web, etc.) clients,
52                             because it can be a lot of data.
53                         resume_attempt => atttempt_id.  Resume verification after a failure.
54                         resume_with_new_attempt => If true, resume from resume_attempt, but
55                             create a new attempt to track the resumption.
56                     /,
57                 type => 'hash'
58             }
59         ],
60         return => {desc => q/
61             Stream of objects containing the number of URLs to be processed (url_count),
62             the number processed thus far including redirects (total_processed),
63             and the current url_verification object (current_verification).
64
65             Note that total_processed may ultimately exceed url_count, since it
66             includes non-anticipate-able redirects.
67
68             The final response contains url_count, total_processed, and the
69             verification_attempt object (attempt).
70             /
71         }
72     }
73 );
74
75 # "verify_session" sounds like something to do with authentication, but it
76 # actually means for a given session, verify all the URLs associated with
77 # that session.
78 sub verify_session {
79     my ($self, $client, $auth, $session_id, $url_ids, $options) = @_;
80     $options ||= {};
81
82     my $e = new_editor(authtoken => $auth, xact => 1);
83     return $e->die_event unless $e->checkauth;
84     return $e->die_event unless $e->allowed('URL_VERIFY');
85
86     my $session = $e->retrieve_url_verify_session($session_id)
87         or return $e->die_event;
88
89     my $attempt_id = $options->{resume_attempt};
90
91     if (!$url_ids) {
92
93         # No URLs provided, load all URLs for the requested session
94
95         my $query = {
96             select => {uvu => ['id']},
97             from => {
98                 uvu => { # url
99                     uvs => { # session
100                         filter => {id => $session_id}
101                     }
102                 }
103             }
104         };
105
106         if ($attempt_id) {
107
108             # when resuming an existing attempt (that presumably failed
109             # mid-processing), we only want to process URLs that either
110             # have no linked url_verification or have an un-completed
111             # url_verification.
112
113             $logger->info("url: resuming attempt $attempt_id");
114
115             $query->{from}->{uvu}->{uvuv} = {
116                 type => 'left',
117                 filter => {attempt => $attempt_id}
118             };
119
120             $query->{where} = {
121                 '+uvuv' => {
122                     '-or' => [
123                         {id => undef}, # no verification started
124                         {res_code => undef} # verification started but did no complete
125                     ]
126                 }
127             };
128
129         } else {
130
131             # this is a new attempt, so we only want to process URLs that
132             # originated from the source records and not from redirects.
133
134             $query->{where} = {
135                 '+uvu' => {redirect_from => undef}
136             };
137         }
138
139         my $ids = $e->json_query($query);
140         $url_ids = [ map {$_->{id}} @$ids ];
141     }
142
143     my $url_count = scalar(@$url_ids);
144     $logger->info("url: processing $url_count URLs");
145
146     my $attempt;
147     if ($attempt_id and !$options->{resume_with_new_attempt}) {
148
149         $attempt = $e->retrieve_url_verification_attempt($attempt_id)
150             or return $e->die_event;
151
152         # no data was written
153         $e->rollback;
154
155     } else {
156
157         $attempt = Fieldmapper::url_verify::verification_attempt->new;
158         $attempt->session($session_id);
159         $attempt->usr($e->requestor->id);
160         $attempt->start_time('now');
161
162         $e->create_url_verify_verification_attempt($attempt)
163             or return $e->die_event;
164
165         $attempt = $e->data;
166         $e->commit;
167     }
168
169     # END DB TRANSACTION
170
171     # Now cycle through the URLs in batches.
172
173     my $batch_size = $U->ou_ancestor_setting_value(
174         $session->owning_lib,
175         'url_verify.verification_batch_size', $e) || 5;
176
177     my $total_excluding_redirects = 0;
178     my $total_processed = 0; # total number processed, including redirects
179     my $resp_window = 1;
180
181     # before we start the real work, let the caller know
182     # the attempt (id) so recovery is possible.
183
184     $client->respond({
185         url_count => $url_count,
186         total_processed => $total_processed,
187         total_excluding_redirects => $total_excluding_redirects,
188         attempt => $attempt
189     });
190
191     my $multises = OpenSRF::MultiSession->new(
192
193         app => 'open-ils.url_verify', # hey, that's us!
194         cap => $batch_size,
195
196         success_handler => sub {
197             my ($self, $req) = @_;
198
199             # API call streams fleshed url_verification objects.  We wrap
200             # those up with some extra info and pass them on to the caller.
201
202             for my $resp (@{$req->{response}}) {
203                 my $content = $resp->content;
204
205                 if ($content) {
206
207                     $total_processed++;
208
209                     if ($options->{report_all} or ($total_processed % $resp_window == 0)) {
210
211                         $client->respond({
212                             url_count => $url_count,
213                             current_verification => $content,
214                             total_excluding_redirects => $total_excluding_redirects,
215                             total_processed => $total_processed
216                         });
217
218                         # start off responding quickly, then throttle
219                         # back to only relaying every 256 messages.
220                         $resp_window *= 2 unless $resp_window >= 256;
221                     }
222                 }
223             }
224         },
225
226         failure_handler => sub {
227             my ($self, $req) = @_;
228
229             # {error} should be an Error w/ a toString
230             $logger->error("url: error processing URL: " . $req->{error});
231         }
232     );
233
234     sort_and_fire_domains(
235         $e, $auth, $attempt, $url_ids, $multises, \$total_excluding_redirects
236     );
237
238     # Wait for all requests to be completed
239     $multises->session_wait(1);
240
241     # All done.  Let's wrap up the attempt.
242     $attempt->finish_time('now');
243
244     $e->xact_begin;
245     $e->update_url_verify_verification_attempt($attempt) or
246         return $e->die_event;
247
248     $e->xact_commit;
249
250     # This way the caller gets an actual timestamp in the "finish_time" field
251     # instead of the string "now".
252     $attempt = $e->retrieve_url_verify_verification_attempt($e->data) or
253         return $e->die_event;
254
255     $e->disconnect;
256
257     return {
258         url_count => $url_count,
259         total_processed => $total_processed,
260         total_excluding_redirects => $total_excluding_redirects,
261         attempt => $attempt
262     };
263 }
264
265 # retrieves the URL domains and sorts them into buckets*
266 # Iterates over the buckets and fires the multi-session call
267 # the main drawback to this domain sorting approach is that
268 # any domain used a lot more than the others will be the
269 # only domain standing after the others are exhausted, which
270 # means it will take a beating at the end of the batch.
271 #
272 # * local data structures, not container.* buckets
273 sub sort_and_fire_domains {
274     my ($e, $auth, $attempt, $url_ids, $multises, $count) = @_;
275
276     # there is potential here for data sets to be too large
277     # for delivery, but it's not likely, since we're only
278     # fetching ID and domain.
279     my $urls = $e->json_query(
280         {
281             select => {uvu => ['id', 'domain']},
282             from => 'uvu',
283             where => {id => $url_ids}
284         },
285         # {substream => 1} only if needed
286     );
287
288     # sort them into buckets based on domain name
289     my %domains;
290     for my $url (@$urls) {
291         $domains{$url->{domain}} = [] unless $domains{$url->{domain}};
292         push(@{$domains{$url->{domain}}}, $url->{id});
293     }
294
295     # loop through the domains and fire the verification call
296     while (keys %domains) {
297         for my $domain (keys %domains) {
298
299             my $url_id = pop(@{$domains{$domain}});
300             delete $domains{$domain} unless @{$domains{$domain}};
301
302             $multises->request(
303                 'open-ils.url_verify.verify_url',
304                 $auth, $attempt->id, $url_id);
305             
306             $$count++;  # sic, a reference to a scalar
307         }
308     }
309 }
310
311
312 # XXX I really want to move this method to open-ils.storage, so we don't have
313 # to authenticate a zillion times. LFW
314
315 __PACKAGE__->register_method(
316     method => 'verify_url',
317     api_name => 'open-ils.url_verify.verify_url',
318     stream => 1,
319     signature => {
320         desc => q/
321             Performs verification of a single URL.  When a redirect is detected,
322             a new URL is created to model the redirect and the redirected URL
323             is then tested, up to max-redirects or a loop is detected.
324         /,
325         params => [
326             {desc => 'Authentication token', type => 'string'},
327             {desc => 'Verification attempt ID (url_verify.verification_attempt.id)', type => 'number'},
328             {desc => 'URL id (url_verify.url.id)', type => 'number'},
329         ],
330         return => {desc => q/Stream of url_verification objects, one per URL tested/}
331     }
332 );
333
334 =head comment
335
336 verification.res_code:
337
338 999 bad hostname, etc. (IO::Socket::Inet errors)
339 998 in-flight errors (e.g connection closed prematurely)
340 997 timeout
341 996 redirect loop
342 995 max redirects
343
344 verification.res_text:
345
346 $@ or custom message "Redirect Loop"
347
348 =cut
349
350 sub verify_url {
351     my ($self, $client, $auth, $attempt_id, $url_id) = @_;
352     my %seen_urls;
353
354     my $e = new_editor(authtoken => $auth);
355     return $e->event unless $e->checkauth;
356
357     my $url = $e->retrieve_url_verify_url($url_id) or return $e->event;
358
359     my ($attempt, $delay, $max_redirects, $timeout) =
360         collect_verify_attempt_and_settings($e, $attempt_id);
361
362     return $e->event unless $e->allowed(
363         'URL_VERIFY', $attempt->session->owning_lib);
364
365     my $cur_url = $url;
366     my $loop_detected = 0;
367     my $redir_count = 0;
368
369     while ($redir_count++ < $max_redirects) {
370
371         if ($seen_urls{$cur_url->full_url}) {
372             $loop_detected = 1;
373             last;
374         }
375
376         $seen_urls{$cur_url->full_url} = $cur_url;
377
378         my $url_resp = verify_one_url($e, $attempt, $cur_url, $timeout);
379
380         # something tragic happened
381         return $url_resp if $U->is_event($url_resp);
382
383         # flesh and respond to the caller
384         $url_resp->{verification}->url($cur_url);
385         $client->respond($url_resp->{verification});
386
387         $cur_url = $url_resp->{redirect_url} or last;
388     }
389
390     if ($loop_detected or $redir_count > $max_redirects) {
391
392         my $vcation = Fieldmapper::url_verify::url_verification->new;
393         $vcation->url($cur_url->id);
394         $vcation->attempt($attempt->id);
395         $vcation->req_time('now');
396
397         if ($loop_detected) {
398             $logger->info("url: redirect loop detected at " . $cur_url->full_url);
399             $vcation->res_code('996');
400             $vcation->res_text('Redirect Loop');
401
402         } else {
403             $logger->info("url: max redirects reached for source URL " . $url->full_url);
404             $vcation->res_code('995');
405             $vcation->res_text('Max Redirects');
406         }
407
408         $e->xact_begin;
409         $e->create_url_verify_url_verification($vcation) or return $e->die_event;
410         $e->xact_commit;
411     }
412
413     # The calling code is likely not multi-threaded, so a
414     # per-URL (i.e. per-thread) delay would not be possible.
415     # Applying the delay here allows the caller to process
416     # batches of URLs without having to worry about the delay.
417     sleep $delay;
418
419     return undef;
420 }
421
422 # temporarily cache some data to avoid a pile
423 # of data lookups on every URL processed.
424 my %cache;
425 sub collect_verify_attempt_and_settings {
426     my ($e, $attempt_id) = @_;
427     my $attempt;
428
429     if (!(keys %cache) or $cache{age} > 20) { # configurable?
430         %cache = (
431             age => 0,
432             attempt => {},
433             delay => {},
434             redirects => {},
435             timeout => {},
436         );
437     }
438
439     if ( !($attempt = $cache{attempt}{$attempt_id}) ) {
440
441         # attempt may have just been created, so
442         # we need to guarantee a write-DB read.
443         $e->xact_begin;
444
445         $attempt =
446             $e->retrieve_url_verify_verification_attempt([
447                 $attempt_id, {
448                     flesh => 1,
449                     flesh_fields => {uvva => ['session']}
450                 }
451             ]) or return $e->die_event;
452
453         $e->rollback;
454
455         $cache{attempt}{$attempt_id} = $attempt;
456     }
457
458     my $org = $attempt->session->owning_lib;
459
460     if (!$cache{timeout}{$org}) {
461
462         $cache{delay}{$org} = $U->ou_ancestor_setting_value(
463             $org, 'url_verify.url_verification_delay', $e);
464
465         # 0 is a valid delay
466         $cache{delay}{$org} = 2 unless defined $cache{delay}{$org};
467
468         $cache{redirects}{$org} = $U->ou_ancestor_setting_value(
469             $org, 'url_verify.url_verification_max_redirects', $e) || 20;
470
471         $cache{timeout}{$org} = $U->ou_ancestor_setting_value(
472             $org, 'url_verify.url_verification_max_wait', $e) || 5;
473
474         $logger->info(
475             sprintf("url: loaded settings delay=%s; max_redirects=%s; timeout=%s",
476                 $cache{delay}{$org}, $cache{redirects}{$org}, $cache{timeout}{$org}));
477     }
478
479     $cache{age}++;
480
481
482     return (
483         $cache{attempt}{$attempt_id},
484         $cache{delay}{$org},
485         $cache{redirects}{$org},
486         $cache{timeout}{$org}
487     );
488 }
489
490
491 # searches for a completed url_verfication for any url processed
492 # within this verification attempt whose full_url matches the
493 # full_url of the provided URL.
494 sub find_matching_url_for_attempt {
495     my ($e, $attempt, $url) = @_;
496
497     my $match = $e->json_query({
498         select => {uvuv => ['id']},
499         from => {
500             uvuv => {
501                 uvva => { # attempt
502                     filter => {id => $attempt->id}
503                 },
504                 uvu => {} # url
505             }
506         },
507         where => {
508             '+uvu' => {
509                 id => {'!=' => $url->id},
510                 full_url => $url->full_url
511             },
512
513             # There could be multiple verifications for matching URLs
514             # We only want a verification that completed.
515             # Note also that 2 identical URLs processed within the same
516             # sub-batch will have to each be fully processed in their own
517             # right, since neither knows how the other will ultimately fare.
518             '+uvuv' => {
519                 res_time => {'!=' => undef}
520             }
521         }
522     })->[0];
523
524     return $e->retrieve_url_verify_url_verification($match->{id}) if $match;
525     return undef;
526 }
527
528
529 =head comment
530
531 1. create the verification object and commit.
532 2. test the URL
533 3. update the verification object to capture the results of the test
534 4. Return redirect_url object if this is a redirect, otherwise undef.
535
536 =cut
537
538 sub verify_one_url {
539     my ($e, $attempt, $url, $timeout) = @_;
540
541     my $url_text = $url->full_url;
542     my $redir_url;
543
544     # first, create the verification object so we can a) indicate that
545     # we're working on this URL and b) get the DB to set the req_time.
546
547     my $vcation = Fieldmapper::url_verify::url_verification->new;
548     $vcation->url($url->id);
549     $vcation->attempt($attempt->id);
550     $vcation->req_time('now');
551
552     # begin phase-I DB communication
553
554     $e->xact_begin;
555
556     my $match_vcation = find_matching_url_for_attempt($e, $attempt, $url);
557
558     if ($match_vcation) {
559         $logger->info("url: found matching URL in verification attempt [$url_text]");
560         $vcation->res_code($match_vcation->res_code);
561         $vcation->res_text($match_vcation->res_text);
562         $vcation->redirect_to($match_vcation->redirect_to);
563     }
564
565     $e->create_url_verify_url_verification($vcation) or return $e->die_event;
566     $e->xact_commit;
567
568     # found a matching URL, no need to re-process
569     return {verification => $vcation} if $match_vcation;
570
571     # End phase-I DB communication
572     # No active DB xact means no cstore timeout concerns.
573
574     # Now test the URL.
575
576     $ENV{FTP_PASSIVE} = 1; # TODO: setting?
577
578     my $ua = LWP::UserAgent->new(
579         ssl_opts => {verify_hostname => 0}, # TODO: verify_hostname setting?
580         agent => $user_agent_string
581     );
582
583     $ua->timeout($timeout);
584
585     my $req = HTTP::Request->new(HEAD => $url->full_url);
586
587     # simple_request avoids LWP's auto-redirect magic
588     my $res = $ua->simple_request($req);
589
590     $logger->info(sprintf(
591         "url: received HTTP '%s' / '%s' [%s]",
592         $res->code,
593         $res->message,
594         $url_text
595     ));
596
597     $vcation->res_code($res->code);
598     $vcation->res_text($res->message);
599
600     # is this a redirect?
601     if ($res->code =~ /^3/) {
602
603         if (my $loc = $res->headers->{location}) {
604             $redir_url = Fieldmapper::url_verify::url->new;
605             $redir_url->session($attempt->session);
606             $redir_url->redirect_from($url->id);
607             $redir_url->full_url($loc);
608
609             $logger->info("url: redirect found $url_text => $loc");
610
611         } else {
612             $logger->info("url: server returned 3XX but no 'Location' header for url $url_text");
613         }
614     }
615
616     # Begin phase-II DB communication
617
618     $e->xact_begin;
619
620     if ($redir_url) {
621         $redir_url = $e->create_url_verify_url($redir_url) or return $e->die_event;
622         $vcation->redirect_to($redir_url->id);
623     }
624
625     $vcation->res_time('now');
626     $e->update_url_verify_url_verification($vcation) or return $e->die_event;
627     $e->commit;
628
629     return {
630         verification => $vcation,
631         redirect_url => $redir_url
632     };
633 }
634
635
636 __PACKAGE__->register_method(
637     method => "create_session",
638     api_name => "open-ils.url_verify.session.create",
639     signature => {
640         desc => q/Create a URL verify session. Also automatically create and
641             link a container./,
642         params => [
643             {desc => "Authentication token", type => "string"},
644             {desc => "session name", type => "string"},
645             {desc => "QueryParser search", type => "string"},
646             {desc => "owning_lib (defaults to ws_ou)", type => "number"},
647         ],
648         return => {desc => "ID of new session or event on error", type => "number"}
649     }
650 );
651
652 sub create_session {
653     my ($self, $client, $auth, $name, $search, $owning_lib) = @_;
654
655     my $e = new_editor(authtoken => $auth, xact => 1);
656     return $e->die_event unless $e->checkauth;
657
658     $owning_lib ||= $e->requestor->ws_ou;
659     return $e->die_event unless $e->allowed("URL_VERIFY", $owning_lib);
660
661     my $session = Fieldmapper::url_verify::session->new;
662     $session->name($name);
663     $session->owning_lib($owning_lib);
664     $session->creator($e->requestor->id);
665     $session->search($search);
666
667     my $container = Fieldmapper::container::biblio_record_entry_bucket->new;
668     $container->btype("url_verify");
669     $container->owner($e->requestor->id);
670     $container->name($name);
671     $container->description("Automatically generated");
672
673     $e->create_container_biblio_record_entry_bucket($container) or
674         return $e->die_event;
675
676     $session->container($e->data->id);
677     $e->create_url_verify_session($session) or
678         return $e->die_event;
679
680     $e->commit or return $e->die_event;
681
682     return $e->data->id;
683 }
684
685 # _check_for_existing_bucket_items() is used later by session_search_and_extract()
686 sub _check_for_existing_bucket_items {
687     my ($e, $session) = @_;
688
689     my $items = $e->json_query(
690         {
691             select => {cbrebi => ['id']},
692             from => {cbrebi => {}},
693             where => {bucket => $session->container},
694             limit => 1
695         }
696     ) or return $e->die_event;
697
698     return new OpenILS::Event("URL_VERIFY_SESSION_ALREADY_SEARCHED") if @$items;
699
700     return;
701 }
702
703 # _get_all_search_results() is used later by session_search_and_extract()
704 sub _get_all_search_results {
705     my ($client, $session) = @_;
706
707     my @result_ids;
708
709     # Don't loop if the user has specified their own offset.
710     if ($session->search =~ /offset\(\d+\)/) {
711         my $res = $U->simplereq(
712             "open-ils.search",
713             "open-ils.search.biblio.multiclass.query.staff",
714             {}, $session->search
715         );
716
717         return new OpenILS::Event("UNKNOWN") unless $res;
718         return $res if $U->is_event($res);
719
720         @result_ids = map { shift @$_ } @{$res->{ids}}; # IDs nested in array
721     } else {
722         my $count;
723         my $so_far = 0;
724
725         LOOP: { do {    # Fun fact: you cannot "last" out of a do/while in Perl
726                         # unless you wrap it another loop structure.
727             my $search = $session->search . " offset(".scalar(@result_ids).")";
728
729             my $res = $U->simplereq(
730                 "open-ils.search",
731                 "open-ils.search.biblio.multiclass.query.staff",
732                 {}, $search
733             );
734
735             return new OpenILS::Event("UNKNOWN") unless $res;
736             return $res if $U->is_event($res);
737
738             # Search only returns the total count when offset is 0.
739             # We can't get more than one superpage this way, XXX TODO ?
740             $count = $res->{count} unless defined $count;
741
742             my @this_batch = map { shift @$_ } @{$res->{ids}}; # unnest IDs
743             push @result_ids, @this_batch;
744
745             # Send a keepalive in case search is slow, although it'll probably
746             # be the query for the first ten results that's slowest.
747             $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
748
749             last unless @this_batch; # Protect against getting fewer results
750                                      # than count promised.
751
752         } while ($count - scalar(@result_ids) > 0); }
753     }
754
755     return (undef, @result_ids);
756 }
757
758
759 __PACKAGE__->register_method(
760     method => "session_search_and_extract",
761     api_name => "open-ils.url_verify.session.search_and_extract",
762     stream => 1,
763     signature => {
764         desc => q/
765             Perform the search contained in the session,
766             populating the linked bucket, and extracting URLs /,
767         params => [
768             {desc => "Authentication token", type => "string"},
769             {desc => "url_verify.session id", type => "number"},
770         ],
771         return => {
772             desc => q/stream of numbers: first number of search results, then
773                 numbers of extracted URLs for each record, grouped into arrays
774                 of 100/,
775             type => "number"
776         }
777     }
778 );
779
780 sub session_search_and_extract {
781     my ($self, $client, $auth, $ses_id) = @_;
782
783     my $e = new_editor(authtoken => $auth);
784     return $e->die_event unless $e->checkauth;
785
786     my $session = $e->retrieve_url_verify_session(int($ses_id));
787
788     return $e->die_event unless
789         $session and $e->allowed("URL_VERIFY", $session->owning_lib);
790
791     if ($session->creator != $e->requestor->id) {
792         $e->disconnect;
793         return new OpenILS::Event("URL_VERIFY_NOT_SESSION_CREATOR");
794     }
795
796     my $delete_error =
797         _check_for_existing_bucket_items($e, $session);
798
799     if ($delete_error) {
800         $e->disconnect;
801         return $delete_error;
802     }
803
804     my ($search_error, @result_ids) =
805         _get_all_search_results($client, $session);
806
807     if ($search_error) {
808         $e->disconnect;
809         return $search_error;
810     }
811
812     $e->xact_begin;
813
814     # Make and save a bucket item for each search result.
815
816     my $pos = 0;
817     my @item_ids;
818
819     # There's an opportunity below to parallelize the extraction of URLs if
820     # we need to.
821
822     foreach my $bre_id (@result_ids) {
823         my $bucket_item =
824             Fieldmapper::container::biblio_record_entry_bucket_item->new;
825
826         $bucket_item->bucket($session->container);
827         $bucket_item->target_biblio_record_entry($bre_id);
828         $bucket_item->pos($pos++);
829
830         $e->create_container_biblio_record_entry_bucket_item($bucket_item) or
831             return $e->die_event;
832
833         push @item_ids, $e->data->id;
834     }
835
836     $e->xact_commit;
837
838     $client->respond($pos); # first response: the number of items created
839                             # (number of search results)
840
841     # For each contain item, extract URLs.  Report counts of URLs extracted
842     # from each record in batches at every hundred records.  XXX Arbitrary.
843
844     my @url_counts;
845     foreach my $item_id (@item_ids) {
846         my $res = $e->json_query({
847             from => ["url_verify.extract_urls", $ses_id, $item_id]
848         }) or return $e->die_event;
849
850         push @url_counts, $res->[0]{"url_verify.extract_urls"};
851
852         if (scalar(@url_counts) % 100 == 0) {
853             $client->respond([ @url_counts ]);
854             @url_counts = ();
855         }
856     }
857
858     $client->respond([ @url_counts ]) if @url_counts;
859
860     $e->disconnect;
861     return;
862 }
863
864
865 1;