package BookExchange; use warnings; use strict; use base 'CGI::Application'; use CGI; use CGI::Carp qw(fatalsToBrowser); use CGI::Application::Session; use Data::Dumper; use DB_File; use FreezeThaw qw(freeze thaw); use HTML::Entities; use Mail::Sendmail; use WWW::Scraper::ISBN; chdir "bookexchange"; my $url = CGI::new->url; sub cgiapp_init { my $self = shift; my $session = $self->session; $session->expires("+20m"); $self->login unless $session->param("user"); } sub setup { my $self = shift; $self->start_mode('search'); $self->run_modes([qw(welcome redirect search view save notify post unpost logout AUTOLOAD)]); } sub AUTOLOAD { my $self = shift; return $self->dump_html; } sub cgiapp_prerun { my $self = shift; my $session = $self->session; my $user = $session->param("user"); unless ($user) { $self->prerun_mode("welcome"); } } sub redirect { my $self = shift; $self->header_type('redirect'); $self->header_props(-url=>$url); } sub teardown { my $self = shift; } sub welcome { my $self = shift; my $session = $self->session; my $query = $self->query(); my $template = $self->load_tmpl('login.tmpl.html'); my $hiddens; foreach my $parameter ($query->param) { my $value = $query->param($parameter); $hiddens .= qq!! unless $value eq "logout"; } $template->param(hiddens => $hiddens); $template->param(scriptname => $url); $template->param(isBadLogin => $session->param("isBadLogin")); $template->param(isGoodLogout => $session->param("isGoodLogout")); $session->clear("isBadLogin"); $session->clear("isGoodLogout"); return $template->output; } sub login { my $self = shift; my $session = $self->session; my $query = $self->query(); my $username = $query->param("name") || ""; my $password = $query->param("password") || ""; $username =~ s/\s//g; tie my %PASSWORDS, "DB_File", "passwords" or die; if (exists $PASSWORDS{$username}) { if ($PASSWORDS{$username} eq $password) { $session->param("user", $username); } else { document("Invalid login from $username"); $session->param("isBadLogin", 1); } } elsif ($username ne "") { $PASSWORDS{$username} = $password; document("Created user $username with password $password"); $session->param("user", $username); } untie %PASSWORDS; return $self->dump_html; } sub search { my $self = shift; my $session = $self->session; my $query = $self->query(); my $searchstring = $query->param("search") || $query->param("isbn") || ""; if (isISBN($searchstring)) { return $self->view; } my @searchterms = split /\s+/, $searchstring; my $template = $self->load_tmpl('search.tmpl.html'); $template->param(scriptname => $url); $template->param(user => $session->param("user")); $template->param(keywords => encode_entities($searchstring)); # physically searching through the books database # and building the hash to be loaded in the template my @loopparameters; my @bookproperties = qw(title author); my @postproperties = qw(seller classes price comments condition); # searching by books first, thawing is expensive # searching by search terms next (cheap) # searching the actual book, first by properties # then maybe by post details tie my %FROZENBOOKS, "DB_File", "books" or die; while (my($isbn, $icybook) = each %FROZENBOOKS) { my %book = %{(thaw $icybook)[0]}; SEARCH: foreach my $search (@searchterms) { # searching book properties foreach my $property (@bookproperties) { # quote meta characters used to block weird search queries if ($book{$property} =~ /\Q$search\E/i) { $book{"isbn"} = $isbn; $book{"scriptname"} = $url; delete $book{"posts"}; push(@loopparameters, \%book); last SEARCH; } } # then by post properties while (my($postid, $post) = each %{$book{"posts"}}) { foreach my $property (@postproperties) { # quote meta characters used to block weird search queries if (${$post}{$property} =~ /\Q$search\E/i) { $book{"isbn"} = $isbn; $book{"scriptname"} = $url; delete $book{"posts"}; push(@loopparameters, \%book); last SEARCH; } } } } } untie %FROZENBOOKS; $template->param(noSearchResults => 1) if not scalar @loopparameters and scalar @searchterms; $template->param(searchResults => \@loopparameters); return $template->output; } sub view { my $self = shift; my $session = $self->session; my $query = $self->query(); my $isbn = $query->param("search") || $query->param("isbn"); # normalize the isbn $isbn =~ s/\D//g; my $template = $self->load_tmpl('view.tmpl.html'); $template->param(isbn => $isbn); $template->param(keywords => $isbn); $template->param(scriptname => $url); my $user = $session->param("user"); $template->param(user => $user); $template->param(isSuccessfulPost => $session->param("isSuccessfulPost")); $template->param(isNotified => $session->param("isNotified")); $template->param(isRemoved => $session->param("isRemoved")); $template->param(isNotEnough => $session->param("isNotEnough")); # clear out the statuses # after they've already been checked $session->clear("isSuccessfulPost"); $session->clear("isNotified"); $session->clear("isRemoved"); $session->clear("isNotEnough"); $template->param(getBookByIsbn($isbn)); # look for posts based on isbn number # unless they're of the user himself # create a template parameter # load the posts into the template tie my %FROZENBOOKS, "DB_File", "books" or die; my %book; %book = %{(thaw $FROZENBOOKS{$isbn})[0]} if exists $FROZENBOOKS{$isbn}; untie %FROZENBOOKS; my @loopparameters; while (my($postid, $post) = each %{$book{"posts"}}) { ${$post}{"isbn"} = $isbn; ${$post}{"id"} = $postid; ${$post}{"title"} = $book{"title"}; ${$post}{"author"} = $book{"author"}; ${$post}{"isOwner"} = (${$post}{"seller"} eq $user); push(@loopparameters, $post); } # sort by price, anonymous routine # scwartzian transform my @sortedbyprice = map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { my $price = ${$_}{"price"}; $price =~ s/[^\d\.]//; [$_, $price]; } @loopparameters; $template->param(searchResults => \@sortedbyprice); $template->param(isNotFound => 1) unless $template->param("title"); return $template->output; } sub save { my $self = shift; my $session = $self->session; my $query = $self->query(); my $isbn = $query->param("isbn"); my @properties = qw/title author/; my %book; for my $property (@properties) { $book{$property} = $query->param($property); } document("Updating database record for $isbn"); freezeBookByIsbn($isbn, \%book); return $self->view; } sub post { my $self = shift; my $session = $self->session; my $query = $self->query(); my @properties = qw/isbn price classes condition comments/; my %post; for my $property (@properties) { $post{$property} = $query->param($property); unless ($post{$property}) { # unless every field is defined # $session->param("isNotEnough", 1); # return $self->view; } } # each post also has a user associated with it # set it and forget it my $seller = $session->param("user"); $post{"seller"} = $seller; my $isbn = $post{"isbn"}; # actually, the isbn data to the post is redundant # so delete it (after its used!!!) delete $post{"isbn"}; # physically post the book tie my %FROZENBOOKS, "DB_File", "books" or die; # just remember that tie can't handle nested structures # but freeze/thaw can # tie uses the isbn number as a key and stops there # set in my %book = ($FROZENBOOKS{$isbn} ? %{(thaw $FROZENBOOKS{$isbn})[0]} : {}); document("Posting " . $book{"title"}); my $postid = rand; # generates a random post id my %postsbyid = ($book{"posts"} ? %{$book{"posts"}} : {}); do { $postid = rand } while exists $postsbyid{$postid}; $postsbyid{$postid} = \%post; #set out $book{"posts"} = \%postsbyid; $FROZENBOOKS{$isbn} = freeze \%book; debug(Dumper \%book . Dumper \%FROZENBOOKS); untie %FROZENBOOKS; $session->param("isSuccessfulPost", 1); return $self->view; } sub notify { my $self = shift; my $session = $self->session; my $query = $self->query(); my $isbn = $query->param("isbn"); my $id = $query->param("id"); tie my %FROZENBOOKS, "DB_File", "books" or die; my %book = ($FROZENBOOKS{$isbn} ? %{(thaw $FROZENBOOKS{$isbn})[0]} : {}); untie %FROZENBOOKS; my %post = %{$book{"posts"}{$id}}; delete $book{"posts"}; my $buyer = $session->param("user"); my $seller = $post{"seller"}; my %notification = ( To => $seller . $domain, From => $buyer . $domain, Subject => "$buyer wants to buy " . $book{"title"}, Message => "Hi! My user name is $buyer and I'm interested in purchasing your book for sale. Please reply back so that we could schedule a time to meet up and trade.\n\n" . scalar Dumper (\%book, \%post) . "If you have already traded this book, please visit $url?rm=search\&isbn=$isbn to remove the post from the website.", ); sendmail %notification or die $Mail::Sendmail::error; document("Sending a notification from $buyer to $seller..."); $session->param("isNotified", 1); return $self->view; } sub unpost { my $self = shift; my $session = $self->session; my $query = $self->query(); my $user = $session->param("user"); my $isbn = $query->param("isbn"); my $id = $query->param("id"); document("Removing a post..."); tie my %FROZENBOOKS, "DB_File", "books" or die; # copy delete thaw etc my %book = ($FROZENBOOKS{$isbn} ? %{(thaw $FROZENBOOKS{$isbn})[0]} : {}); # just check that the user signed in # is the same as the user mentioned in the post my $seller = $book{"posts"}{$id}{"seller"}; delete $book{"posts"}{$id} if $user eq $seller; # commit the change $FROZENBOOKS{$isbn} = freeze \%book; untie %FROZENBOOKS; $session->param("isRemoved", 1); return $self->view; } sub logout { my $self = shift; my $session = $self->session; $session->clear(["user"]); $session->param("isGoodLogout", 1); return $self->welcome; } sub debug { my $message = shift; open LOG, ">debug.txt" or die; print LOG scalar localtime() . ": $message\n"; close LOG; } sub document { my $message = shift; my $cgi = new CGI; my $ipaddress = $cgi->remote_host(); open LOG, ">>log.txt" or die; print LOG scalar localtime() . " \@ $ipaddress: $message\n"; close LOG; } sub isISBN { my $isbnumber = shift; return $isbnumber =~ /(\d.*){10}/; } sub getBookByIsbn { my $isbn = shift; return unless $isbn; tie my %FROZENBOOKS, "DB_File", "books" or die; if (exists $FROZENBOOKS{$isbn}) { # thaw always returns a list my $bookwithposts = (thaw $FROZENBOOKS{$isbn})[0]; delete ${$bookwithposts}{"posts"}; return $bookwithposts; } else { document("Performing remote search for $isbn"); my $scraper = WWW::Scraper::ISBN->new(); $scraper->drivers("LOC", "ISBNnu"); my $record = $scraper->search($isbn); if ($record->found) { my %book = %{$record->book}; delete $book{"edition"}; delete $book{"volume"}; freezeBookByIsbn($isbn, \%book); my %bookwithoutposts = %book; delete $bookwithoutposts{"posts"}; return \%bookwithoutposts; } else { return {} } } untie %FROZENBOOKS; } sub freezeBookByIsbn { my $isbn = shift; my $book = shift; delete ${$book}{"isbn"}; ${$book}{"posts"} = {}; tie my %FROZENBOOKS, "DB_File", "books" or die; $FROZENBOOKS{$isbn} = freeze $book; untie %FROZENBOOKS; } 1;