package Bookmarks::Controller; use Moose; use Encode; use HTTP::Date qw{time2str str2time}; use JSON; use Bookmarks; use Bookmarks::List; use URI; use Template; has dbname => ( is => 'ro', required => 1, ); has bookmarks => ( is => 'ro', handles => [qw{get_bookmark}], builder => '_build_bookmarks', lazy => 1, ); has base_uri => ( is => 'ro', builder => '_build_base_uri', lazy => 1, ); has request => ( is => 'rw', ); sub _build_bookmarks { my $self = shift; return Bookmarks->new({ dbname => $self->dbname, base_uri => $self->base_uri, }); } sub _build_base_uri { my $self = shift; my $url = $self->request->base; $url .= '/' unless $url =~ m{/$}; return URI->new($url); } sub find_or_new { my $self = shift; my $bookmark = $self->bookmarks->get_bookmark({ uri => $self->request->param('uri') }); if ($bookmark) { # redirect to the view of the existing bookmark return [301, [Location => $bookmark->bookmark_uri], []]; } else { # bookmark was not found; show the form to create a new bookmark require File::Basename; my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) }); $template->process( 'bookmark.tt', { bookmark => { uri => $self->request->param('uri'), title => $self->request->param('title') || '', }, }, \my $output, ); return [404, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; } } sub _get_search_from_request { my $self = shift; my @tags = grep { $_ ne '' } $self->request->param('tag'); my $query = $self->request->param('q'); my $limit = $self->request->param('limit'); my $offset = $self->request->param('offset'); my $page = $self->request->param('page'); return $self->bookmarks->search({ query => $query, tags => \@tags, limit => $limit, offset => $offset, page => $page, }); } sub list { my $self = shift; # list all the bookmarks my $mtime = $self->bookmarks->get_last_modified_time; my $format = $self->request->param('format') || 'html'; my $list = Bookmarks::List->new({ bookmarks => $self->bookmarks, search => $self->_get_search_from_request, }); my $as_format = "as_$format"; if (!$list->meta->has_method($as_format)) { return [406, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$format" is not a supported format}]]; } return $list->$as_format; } sub sidebar { my $self = shift; my $list = Bookmarks::List->new({ bookmarks => $self->bookmarks, search => $self->_get_search_from_request, }); require Template; require File::Basename; my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/List.pm'}) }); my @all_tags = $list->bookmarks->get_tags({ selected => @{ $list->tags }[0] }); my @cotags = $list->bookmarks->get_cotags({ search => $list->search }); $template->process( 'list.tt', { base_url => $list->bookmarks->base_uri, title => $list->title, query => $list->query, selected_tag => @{ $list->tags }[0], search_tags => $list->tags, links => [ $list->_get_list_links('text/html', { q => $list->query, tag => $list->tags }) ], all_tags => \@all_tags, cotags => \@cotags, resources => $list->results, pages => $list->search->pages, }, \my $output, ); return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; } sub feed { my $self = shift; my $query = $self->request->param('q'); my @tags = grep { $_ ne '' } $self->request->param('tag'); # construct a feed from the most recent 12 bookmarks my $list = Bookmarks::List->new({ bookmarks => $self->bookmarks, search => $self->bookmarks->search({ query => $query, tags => \@tags, limit => 12 }), }); return $list->as_atom; } # returns 1 if there is an If-Modified-Since header and it is newer than the given $mtime # returns 0 if there is an If-Modified-Since header but the $mtime is newer # returns undef if there is no If-Modified-Since header sub _request_is_newer_than { my $self = shift; my $mtime = shift; # check If-Modified-Since header to return cache response if ($self->request->env->{HTTP_IF_MODIFIED_SINCE}) { my $cache_time = str2time($self->request->env->{HTTP_IF_MODIFIED_SINCE}); return $mtime <= $cache_time ? 1 : 0; } else { return; } } sub view { my ($self, $id) = @_; my $format = $self->request->param('format') || 'html'; my $bookmark = $self->get_bookmark({ id => $id }); if ($bookmark) { return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime); my $last_modified = time2str($bookmark->mtime); if ($format eq 'json') { my $json = decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark->to_hashref)); return [200, ['Content-Type' => 'application/json; charset=UTF-8', 'Last-Modified' => $last_modified], [$json]]; } else { # display the bookmark form for this bookmark require File::Basename; my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) }); $template->process( 'bookmark.tt', { bookmark => $bookmark }, \my $output, ); return [200, ['Content-Type' => 'text/html; charset=UTF-8', 'Last-Modified' => $last_modified], [$output]]; } } else { return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]]; } } sub view_field { my ($self, $id, $field) = @_; my $bookmark = $self->get_bookmark({ id => $id }); if ($bookmark) { return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime); # check whether the requested field is part of the bookmark if (!$bookmark->meta->has_attribute($field)) { return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$field" is not a valid bookmark data field}]]; } # respond with just the requested field as plain text my $value = $bookmark->$field; my $last_modified = time2str($bookmark->mtime); return [200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Last-Modified' => $last_modified], [ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value]]; } else { return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]]; } } sub create_and_redirect { my $self = shift; my $uri = $self->request->param('uri'); my $title = $self->request->param('title'); my @tags = split ' ', $self->request->param('tags'); my $bookmark = $self->bookmarks->add({ uri => $uri, title => $title, tags => \@tags, }); #TODO: not RESTful; the proper RESTful response would be a 201 return [303, ['Location' => $bookmark->bookmark_uri->canonical], []]; } sub update_and_redirect { my $self = shift; my $id = shift; my $bookmark = $self->bookmarks->get_bookmark({ id => $id }); if ($bookmark) { # update the URI, title, and tags $bookmark->uri($self->request->param('uri')); $bookmark->title($self->request->param('title')); $bookmark->tags([ split ' ', $self->request->param('tags') || '' ]); # write to the database $bookmark->update; #TODO: not RESTful; proper response would be a 200 return [303, ['Location' => $bookmark->bookmark_uri->canonical], []]; } else { return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]]; } } sub tag_tree { my $self = shift; my $tags = shift || []; my $list = Bookmarks::List->new({ bookmarks => $self->bookmarks, search => $self->bookmarks->search({ tags => $tags, }), }); my @tags = map { { path => join('/', @$tags[0 .. $_ - 1]), tag => $tags->[$_ - 1], } } 1 .. scalar(@{ $tags }); require Template; require File::Basename; my @cotags = $self->bookmarks->get_cotags({ search => $list->search }); #TODO: get the actual request URI and makre sure it ends with a / my $base_url = join '/', ($self->base_uri . 'tags'), @$tags; $base_url .= '/'; my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) }); $template->process( 'tagtree.tt', { base_url => $self->base_uri, request_url => $base_url, tags => \@tags, cotags => \@cotags, bookmarks => $list->results, }, \my $output, ); return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; } 1;