source: bookmarks/trunk/BookmarkApp.pm @ 27

Last change on this file since 27 was 27, checked in by peter, 11 years ago
  • protect the specific field request with an eval; if it fails because the method is not found, respond with a 404
  • use the JSON convert_blessed in the single bookmark view
  • use the accessors instead of the direct hash calls for ctime and mtime
File size: 9.2 KB
Line 
1package BookmarkApp;
2use strict;
3use warnings;
4use base qw{CGI::Application};
5
6use CGI::Application::Plugin::TT;
7
8use Encode;
9use HTTP::Date qw{time2isoz};
10use JSON;
11use Bookmarks;
12
13use URI;
14my $base_uri = URI->new;
15$base_uri->scheme('http');
16$base_uri->host($ENV{HTTP_X_FORWARDED_HOST} || $ENV{SERVER_NAME});
17$base_uri->port($ENV{SERVER_PORT});
18$base_uri->path($ENV{SCRIPT_NAME} . '/');
19
20my $dbname = 'fk.db';
21my $bookmarks = Bookmarks->new({
22    dbname   => $dbname,
23    base_uri => $base_uri,
24});
25
26sub setup {
27    my $self = shift;
28    $self->mode_param(path_info => 1);
29    $self->run_modes([qw{
30        list
31        feed
32        view
33        edit
34    }]);
35}
36
37sub list {
38    my $self = shift;
39    my $q = $self->query;
40
41    # check for a uri param, and if there is one present,
42    # see if a bookmark for that URI already exists
43    if (defined(my $uri = $q->param('uri'))) {
44        my $bookmark = $bookmarks->get_bookmark({ uri => $uri });
45        if ($bookmark) {
46            # redirect to the view of the existing bookmark
47            $self->header_type('redirect');
48            $self->header_props(
49                -uri => $q->url . '/' . $bookmark->{id},
50            );
51            return;
52        } else {
53            # bookmark was not found; show the form to create a new bookmark
54            $bookmark->{uri} = $uri;
55            $bookmark->{title} = $q->param('title');
56            $self->header_props(
57                -type    => 'text/html',
58                -charset => 'UTF-8',
59                -status  => 404,
60            );
61            return $self->tt_process(
62                'bookmark.tt',
63                $bookmark,
64            );
65        }
66    }
67
68    # list all the bookmarks
69    my $format = $q->param('format') || 'html';
70    my $tag = $q->param('tag');
71    my @tags = $q->param('tag');
72    # special case: handle the empty tag
73    if (@tags == 1 && $tags[0] eq '') {
74        @tags = ();
75    }
76    my $limit = $q->param('limit');
77    my $offset = $q->param('offset');
78    my @resources = $bookmarks->get_bookmarks({
79        tag    => \@tags,
80        limit  => $limit,
81        offset => $offset,
82    });
83    my @all_tags = $bookmarks->get_tags({ selected => $tag });
84    my @cotags = $bookmarks->get_cotags({ tag => \@tags });
85
86    if ($format eq 'json') {
87        $self->header_props(
88            -type    => 'application/json',
89            -charset => 'UTF-8',
90        );
91        return decode_utf8(
92            JSON->new->utf8->convert_blessed->encode({
93                bookmarks => \@resources,
94            })
95        );
96    } elsif ($format eq 'xbel') {
97        require XML::XBEL;
98        #TODO: conditional support; if XML::XBEL is not present, return a 5xx response
99
100        my $xbel = XML::XBEL->new;
101
102        $xbel->new_document({
103            title => 'Bookmarks' . ($tag ? " tagged as $tag" : ''),
104        });
105
106        for my $bookmark (@resources) {
107            my $cdatetime = time2isoz $bookmark->{ctime};
108            my $mdatetime = time2isoz $bookmark->{mtime};
109            # make the timestamps W3C-correct
110            s/ /T/ foreach ($cdatetime, $mdatetime);
111
112            $xbel->add_bookmark({
113                href     => $bookmark->{uri},
114                title    => $bookmark->{title},
115                desc     => 'Tags: ' . join(', ', @{ $bookmark->{tags} }),
116                added    => $cdatetime,
117                #XXX: are we sure that modified is the mtime of the bookmark or the resource?
118                modified => $mdatetime,
119            });
120        }
121
122        $self->header_props(
123            -type    => 'application/xml',
124            -charset => 'UTF-8',
125        );
126
127        return $xbel->toString;
128    } else {
129        $self->header_props(
130            -type    => 'text/html',
131            -charset => 'UTF-8',
132        );
133
134        # set the base URL, adding a trailing slash if needed
135        my $base_url = $q->url;
136        $base_url .= '/' if $base_url =~ m{/bookmarks$};
137
138        my @links = (
139            {
140                text => 'Link',
141                type => 'text/html',
142                rel  => 'self',
143                query => {
144                    tag => \@tags,
145                },
146            },
147            {
148                text => 'JSON',
149                type => 'application/json',
150                rel  => 'alternate',
151                query => {
152                    tag => \@tags,
153                    format => 'json',
154                },
155            },
156            {
157                text => 'XBEL',
158                type => 'application/xml',
159                rel  => 'alternate',
160                query => {
161                    tag => \@tags,
162                    format => 'xbel',
163                },
164            },
165            {
166                text => 'Atom',
167                type => 'application/atom+xml',
168                rel  => 'alternate',
169                path => 'feed',
170                query => {
171                    tag => \@tags,
172                },
173            },
174        );
175        for my $link (@links) {
176            $link->{href} = URI->new_abs($link->{path} || '', $base_uri);
177            $link->{href}->query_form($link->{query});
178        }
179       
180        return $self->tt_process(
181            'list.tt',
182            {
183                base_url     => $base_url,
184                selected_tag => $tag,
185                search_tags  => \@tags,
186                links        => \@links,
187                all_tags     => \@all_tags,
188                cotags       => \@cotags,
189                resources    => \@resources,
190            },
191        );
192    }
193}
194
195sub feed {
196    my $self = shift;
197    my $q = $self->query;
198
199    my $tag = $q->param('tag');
200
201    require XML::Atom::Feed;
202    require XML::Atom::Entry;
203    require XML::Atom::Link;
204
205    my $feed = XML::Atom::Feed->new;
206    $feed->title('Bookmarks' . ($tag ? " tagged as $tag" : ''));
207    $feed->id($base_uri->canonical . 'feed');
208
209    # construct a feed from the most recent 12 bookmarks
210    for my $bookmark ($bookmarks->get_bookmarks({ tag => $tag, limit => 12 })) {
211        my $entry = XML::Atom::Entry->new;
212        $entry->id($bookmark->{bookmark_uri});
213        $entry->title($bookmark->{title});
214        my $link = XML::Atom::Link->new;
215        $link->href($bookmark->{uri});
216        $entry->add_link($link);
217        $entry->summary('Tags: ' . join(', ', @{ $bookmark->{tags} }));
218        $feed->add_entry($entry);
219    }
220
221    $self->header_props(
222        -type => 'application/atom+xml',
223        -charset => 'UTF-8',
224    );
225    return $feed->as_xml;
226}
227
228sub view {
229    my $self = shift;
230    my $id = $self->param('id');
231    my $field = $self->param('field');
232    my $format = $self->query->param('format') || 'html';
233
234    my $bookmark = $bookmarks->get_bookmark({ id => $id });
235    if ($bookmark) {
236        if ($field) {
237            # respond with just the requested field as plain text
238            my $value = eval { $bookmark->$field };
239            if ($@) {
240                if ($@ =~ /Can't locate object method/) {
241                    $self->header_props(
242                        -type    => 'text/plain',
243                        -charset => 'UTF-8',
244                        -status  => 404,
245                    );
246                    return qq{"$field" is not a valid bookmark data field};
247                } else {
248                    die $@;
249                }
250            }
251            $self->header_props(
252                -type    => 'text/plain',
253                -charset => 'UTF-8',
254            );
255            return ref $value eq 'ARRAY' ? join(',', @{ $value }) : $value;
256        } else {
257            if ($format eq 'json') {
258                $self->header_props(
259                    -type    => 'application/json',
260                    -charset => 'UTF-8',
261                );
262                return decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark));
263            } else {
264                # display the bookmark form for this bookmark
265                $bookmark->{exists} = 1;
266                $bookmark->{created} = "Created " . localtime($bookmark->ctime);
267                $bookmark->{created} .= '; Updated ' . localtime($bookmark->mtime) unless $bookmark->ctime == $bookmark->mtime;
268                $self->header_props(
269                    -type    => 'text/html',
270                    -charset => 'UTF-8',
271                );
272                return $self->tt_process(
273                    'bookmark.tt',
274                    $bookmark,
275                );
276            }
277        }
278    } else {
279        $self->header_props(
280            -type    => 'text/html',
281            -charset => 'UTF-8',
282            -status  => 404,
283        );
284        return "Bookmark $id Not Found";
285    }
286}
287
288#TODO: split this into edit and create methods
289sub edit {
290    my $self = shift;
291    my $q = $self->query;
292    #TODO: get the bookmark based on the id and edit it directly?
293    #TODO: deal with changing URIs
294    my $uri = $q->param('uri');
295    my $title = $q->param('title');
296    my @tags = split ' ', $q->param('tags');
297    $bookmarks->add({
298        uri   => $uri,
299        title => $title,
300        tags  => \@tags,
301    });
302
303=begin
304
305    my $location = URI->new($q->url);
306    $location->query_form(uri => $uri) if defined $q->url_param('uri');
307    $location->fragment('updated');
308
309=cut
310
311    # return to the form
312    $self->header_type('redirect');
313    $self->header_props(
314        -uri => $ENV{REQUEST_URI},
315        -status => 303,
316    );
317}
318
3191;
Note: See TracBrowser for help on using the repository browser.