package BookmarkApp; use strict; use warnings; use base qw{CGI::Application}; use CGI::Application::Plugin::TT; use Encode; use HTTP::Date qw{time2isoz}; use JSON; use Bookmarks; use URI; my $base_uri = URI->new; $base_uri->scheme('http'); $base_uri->host($ENV{HTTP_X_FORWARDED_HOST} || $ENV{SERVER_NAME}); $base_uri->port($ENV{SERVER_PORT}); $base_uri->path($ENV{SCRIPT_NAME} . '/'); my $dbname = 'fk.db'; my $bookmarks = Bookmarks->new({ dbname => $dbname, base_uri => $base_uri->canonical, }); sub setup { my $self = shift; $self->mode_param(path_info => 1); $self->run_modes([qw{ list feed view edit }]); } sub list { my $self = shift; my $q = $self->query; # check for a uri param, and if there is one present, # see if a bookmark for that URI already exists if (defined(my $uri = $q->param('uri'))) { my $bookmark = $bookmarks->get_bookmark({ uri => $uri }); if ($bookmark) { # redirect to the view of the existing bookmark $self->header_type('redirect'); $self->header_props( -uri => $q->url . '/' . $bookmark->{id}, ); return; } else { # bookmark was not found; show the form to create a new bookmark $bookmark->{uri} = $uri; $bookmark->{title} = $q->param('title'); $self->header_props( -type => 'text/html', -charset => 'UTF-8', -status => 404, ); return $self->tt_process( 'bookmark.tt', $bookmark, ); } } # list all the bookmarks my $format = $q->param('format') || 'html'; my $tag = $q->param('tag'); my @tags = $q->param('tag'); # special case: handle the empty tag if (@tags == 1 && $tags[0] eq '') { @tags = (); } my $limit = $q->param('limit'); my $offset = $q->param('offset'); my @resources = $bookmarks->get_resources({ tag => \@tags, limit => $limit, offset => $offset, }); my @all_tags = $bookmarks->get_tags({ selected => $tag }); my @cotags = $bookmarks->get_cotags({ tag => \@tags }); if ($format eq 'json') { $self->header_props( -type => 'application/json', -charset => 'UTF-8', ); return decode_utf8( encode_json({ bookmarks => \@resources, }) ); } elsif ($format eq 'xbel') { require XML::XBEL; #TODO: conditional support; if XML::XBEL is not present, return a 5xx response my $xbel = XML::XBEL->new; $xbel->new_document({ title => 'Bookmarks' . ($tag ? " tagged as $tag" : ''), }); for my $bookmark (@resources) { my $cdatetime = time2isoz $bookmark->{ctime}; my $mdatetime = time2isoz $bookmark->{mtime}; # make the timestamps W3C-correct s/ /T/ foreach ($cdatetime, $mdatetime); $xbel->add_bookmark({ href => $bookmark->{uri}, title => $bookmark->{title}, desc => 'Tags: ' . join(', ', @{ $bookmark->{tags} }), added => $cdatetime, #XXX: are we sure that modified is the mtime of the bookmark or the resource? modified => $mdatetime, }); } $self->header_props( -type => 'application/xml', -charset => 'UTF-8', ); return $xbel->toString; } else { $self->header_props( -type => 'text/html', -charset => 'UTF-8', ); # set the base URL, adding a trailing slash if needed my $base_url = $q->url; $base_url .= '/' if $base_url =~ m{/bookmarks$}; my @links = ( { text => 'Link', type => 'text/html', rel => 'self', query => { tag => \@tags, }, }, { text => 'JSON', type => 'application/json', rel => 'alternate', query => { tag => \@tags, format => 'json', }, }, { text => 'XBEL', type => 'application/xml', rel => 'alternate', query => { tag => \@tags, format => 'xbel', }, }, { text => 'Atom', type => 'application/atom+xml', rel => 'alternate', path => 'feed', query => { tag => \@tags, }, }, ); for my $link (@links) { $link->{href} = URI->new_abs($link->{path} || '', $base_uri); $link->{href}->query_form($link->{query}); } return $self->tt_process( 'list.tt', { base_url => $base_url, selected_tag => $tag, search_tags => \@tags, links => \@links, all_tags => \@all_tags, cotags => \@cotags, resources => \@resources, }, ); } } sub feed { my $self = shift; my $q = $self->query; my $tag = $q->param('tag'); require XML::Atom::Feed; require XML::Atom::Entry; require XML::Atom::Link; my $feed = XML::Atom::Feed->new; $feed->title('Bookmarks' . ($tag ? " tagged as $tag" : '')); $feed->id($base_uri->canonical . 'feed'); # construct a feed from the most recent 12 bookmarks for my $bookmark ($bookmarks->get_resources({ tag => $tag, limit => 12 })) { my $entry = XML::Atom::Entry->new; $entry->id($bookmark->{bookmark_uri}); $entry->title($bookmark->{title}); my $link = XML::Atom::Link->new; $link->href($bookmark->{uri}); $entry->add_link($link); $entry->summary('Tags: ' . join(', ', @{ $bookmark->{tags} })); $feed->add_entry($entry); } $self->header_props( -type => 'application/atom+xml', -charset => 'UTF-8', ); return $feed->as_xml; } sub view { my $self = shift; my $id = $self->param('id'); my $field = $self->param('field'); my $format = $self->query->param('format') || 'html'; my $bookmark = $bookmarks->get_bookmark({ id => $id }); if ($bookmark) { if ($field) { # respond with just the requested field as plain text $self->header_props( -type => 'text/plain', -charset => 'UTF-8', ); my $value = $bookmark->{$field}; return ref $value eq 'ARRAY' ? join(',', @{ $value }) : $value; } else { if ($format eq 'json') { $self->header_props( -type => 'application/json', -charset => 'UTF-8', ); return decode_utf8(encode_json($bookmark)); } else { # display the bookmark form for this bookmark $bookmark->{exists} = 1; $bookmark->{created} = "Created " . localtime($bookmark->{ctime}); $bookmark->{created} .= '; Updated ' . localtime($bookmark->{mtime}) unless $bookmark->{ctime} == $bookmark->{mtime}; $self->header_props( -type => 'text/html', -charset => 'UTF-8', ); return $self->tt_process( 'bookmark.tt', $bookmark, ); } } } else { $self->header_props( -type => 'text/html', -charset => 'UTF-8', -status => 404, ); return "Bookmark $id Not Found"; } } #TODO: split this into edit and create methods sub edit { my $self = shift; my $q = $self->query; #TODO: get the bookmark based on the id and edit it directly? #TODO: deal with changing URIs my $uri = $q->param('uri'); my $title = $q->param('title'); my @tags = split ' ', $q->param('tags'); $bookmarks->add({ uri => $uri, title => $title, tags => \@tags, }); =begin my $location = URI->new($q->url); $location->query_form(uri => $uri) if defined $q->url_param('uri'); $location->fragment('updated'); =cut # return to the form $self->header_type('redirect'); $self->header_props( -uri => $ENV{REQUEST_URI}, -status => 303, ); } 1;