| [59] | 1 | package BookmarkController; | 
|---|
|  | 2 | use Moose; | 
|---|
| [5] | 3 |  | 
|---|
|  | 4 | use Encode; | 
|---|
| [57] | 5 | use HTTP::Date qw{time2isoz time2iso time2str str2time}; | 
|---|
| [5] | 6 | use JSON; | 
|---|
|  | 7 | use Bookmarks; | 
|---|
| [7] | 8 | use URI; | 
|---|
| [59] | 9 | use Template; | 
|---|
| [7] | 10 |  | 
|---|
| [61] | 11 | has dbname => ( | 
|---|
|  | 12 | is => 'ro', | 
|---|
|  | 13 | required => 1, | 
|---|
|  | 14 | ); | 
|---|
| [59] | 15 | has bookmarks => ( | 
|---|
| [61] | 16 | is => 'ro', | 
|---|
| [59] | 17 | handles => [qw{get_bookmark}], | 
|---|
| [61] | 18 | builder => '_build_bookmarks', | 
|---|
|  | 19 | lazy => 1, | 
|---|
| [59] | 20 | ); | 
|---|
|  | 21 | has base_uri => ( | 
|---|
|  | 22 | is => 'ro', | 
|---|
|  | 23 | builder => '_build_base_uri', | 
|---|
|  | 24 | lazy => 1, | 
|---|
|  | 25 | ); | 
|---|
|  | 26 | has request => ( | 
|---|
|  | 27 | is => 'ro', | 
|---|
| [61] | 28 | required => 1, | 
|---|
| [59] | 29 | ); | 
|---|
|  | 30 |  | 
|---|
| [61] | 31 | sub _build_bookmarks { | 
|---|
|  | 32 | my $self = shift; | 
|---|
|  | 33 | return Bookmarks->new({ | 
|---|
|  | 34 | dbname   => $self->dbname, | 
|---|
|  | 35 | base_uri => $self->base_uri, | 
|---|
|  | 36 | }); | 
|---|
|  | 37 | } | 
|---|
|  | 38 |  | 
|---|
| [59] | 39 | sub _build_base_uri { | 
|---|
| [5] | 40 | my $self = shift; | 
|---|
| [59] | 41 | my $url = $self->request->base; | 
|---|
| [41] | 42 |  | 
|---|
| [53] | 43 | $url .= '/' unless $url =~ m{/$}; | 
|---|
| [59] | 44 | return URI->new($url); | 
|---|
| [5] | 45 | } | 
|---|
|  | 46 |  | 
|---|
| [38] | 47 | sub _get_list_links { | 
|---|
|  | 48 | my $self = shift; | 
|---|
| [52] | 49 | my ($self_type, $query) = @_; | 
|---|
| [38] | 50 | my @links = ( | 
|---|
|  | 51 | { | 
|---|
|  | 52 | text => 'JSON', | 
|---|
|  | 53 | type => 'application/json', | 
|---|
|  | 54 | query => { | 
|---|
| [52] | 55 | %$query, | 
|---|
| [38] | 56 | format => 'json', | 
|---|
|  | 57 | }, | 
|---|
|  | 58 | }, | 
|---|
|  | 59 | { | 
|---|
|  | 60 | text => 'XBEL', | 
|---|
|  | 61 | type => 'application/xml', | 
|---|
|  | 62 | query => { | 
|---|
| [52] | 63 | %$query, | 
|---|
| [38] | 64 | format => 'xbel', | 
|---|
|  | 65 | }, | 
|---|
|  | 66 | }, | 
|---|
|  | 67 | { | 
|---|
|  | 68 | text => 'Atom', | 
|---|
|  | 69 | type => 'application/atom+xml', | 
|---|
|  | 70 | path => 'feed', | 
|---|
|  | 71 | query => { | 
|---|
| [52] | 72 | %$query, | 
|---|
| [38] | 73 | }, | 
|---|
|  | 74 | }, | 
|---|
|  | 75 | { | 
|---|
| [49] | 76 | text => 'CSV', | 
|---|
|  | 77 | type => 'text/csv', | 
|---|
|  | 78 | query => { | 
|---|
| [52] | 79 | %$query, | 
|---|
| [49] | 80 | format => 'csv', | 
|---|
|  | 81 | }, | 
|---|
|  | 82 | }, | 
|---|
|  | 83 | { | 
|---|
| [38] | 84 | text => 'URI List', | 
|---|
|  | 85 | type => 'text/uri-list', | 
|---|
|  | 86 | query => { | 
|---|
| [52] | 87 | %$query, | 
|---|
| [40] | 88 | format => 'text', | 
|---|
| [38] | 89 | }, | 
|---|
| [49] | 90 | }, | 
|---|
| [50] | 91 | { | 
|---|
|  | 92 | text => 'HTML', | 
|---|
|  | 93 | type => 'text/html', | 
|---|
|  | 94 | query => { | 
|---|
| [52] | 95 | %$query, | 
|---|
| [50] | 96 | }, | 
|---|
|  | 97 | }, | 
|---|
| [38] | 98 | ); | 
|---|
|  | 99 |  | 
|---|
|  | 100 | for my $link (@links) { | 
|---|
|  | 101 | $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate'; | 
|---|
| [59] | 102 | $link->{href} = URI->new_abs($link->{path} || '', $self->base_uri); | 
|---|
| [38] | 103 | $link->{href}->query_form($link->{query}); | 
|---|
|  | 104 | } | 
|---|
|  | 105 |  | 
|---|
|  | 106 | return @links; | 
|---|
|  | 107 | } | 
|---|
|  | 108 |  | 
|---|
| [59] | 109 | sub find_or_new { | 
|---|
| [5] | 110 | my $self = shift; | 
|---|
|  | 111 |  | 
|---|
| [59] | 112 | my $bookmark = $self->bookmarks->get_bookmark({ uri => $self->request->param('uri') }); | 
|---|
|  | 113 | if ($bookmark) { | 
|---|
|  | 114 | # redirect to the view of the existing bookmark | 
|---|
|  | 115 | return [301, [Location => $bookmark->bookmark_uri], []]; | 
|---|
|  | 116 | } else { | 
|---|
|  | 117 | # bookmark was not found; show the form to create a new bookmark | 
|---|
|  | 118 | my $template = Template->new; | 
|---|
|  | 119 | $template->process( | 
|---|
|  | 120 | 'bookmark.tt', | 
|---|
| [66] | 121 | { | 
|---|
|  | 122 | bookmark => { | 
|---|
|  | 123 | uri   => $self->request->param('uri'), | 
|---|
|  | 124 | title => $self->request->param('title') || '', | 
|---|
|  | 125 | }, | 
|---|
| [59] | 126 | }, | 
|---|
|  | 127 | \my $output, | 
|---|
|  | 128 | ); | 
|---|
|  | 129 | return [404, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; | 
|---|
| [5] | 130 | } | 
|---|
| [59] | 131 | } | 
|---|
| [5] | 132 |  | 
|---|
| [59] | 133 | sub list { | 
|---|
|  | 134 | my $self = shift; | 
|---|
|  | 135 |  | 
|---|
| [5] | 136 | # list all the bookmarks | 
|---|
| [59] | 137 | my $mtime = $self->bookmarks->get_last_modified_time; | 
|---|
| [57] | 138 |  | 
|---|
| [59] | 139 | my $format = $self->request->param('format') || 'html'; | 
|---|
| [35] | 140 |  | 
|---|
| [59] | 141 | my @tags = grep { $_ ne '' } $self->request->param('tag'); | 
|---|
|  | 142 | my $query = $self->request->param('q'); | 
|---|
|  | 143 | my $limit = $self->request->param('limit'); | 
|---|
|  | 144 | my $offset = $self->request->param('offset'); | 
|---|
|  | 145 | my @resources = $self->bookmarks->get_bookmarks({ | 
|---|
| [52] | 146 | query  => $query, | 
|---|
| [20] | 147 | tag    => \@tags, | 
|---|
| [13] | 148 | limit  => $limit, | 
|---|
|  | 149 | offset => $offset, | 
|---|
|  | 150 | }); | 
|---|
| [59] | 151 | my @all_tags = $self->bookmarks->get_tags({ selected => $tags[0] }); | 
|---|
|  | 152 | my @cotags = $self->bookmarks->get_cotags({ | 
|---|
| [52] | 153 | query  => $query, | 
|---|
|  | 154 | tag    => \@tags, | 
|---|
|  | 155 | }); | 
|---|
| [31] | 156 |  | 
|---|
| [52] | 157 | my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : ''); | 
|---|
| [5] | 158 |  | 
|---|
|  | 159 | if ($format eq 'json') { | 
|---|
| [59] | 160 | my $json = decode_utf8( | 
|---|
| [25] | 161 | JSON->new->utf8->convert_blessed->encode({ | 
|---|
| [7] | 162 | bookmarks => \@resources, | 
|---|
| [5] | 163 | }) | 
|---|
|  | 164 | ); | 
|---|
| [59] | 165 | return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]]; | 
|---|
| [19] | 166 | } elsif ($format eq 'xbel') { | 
|---|
|  | 167 | require XML::XBEL; | 
|---|
|  | 168 | #TODO: conditional support; if XML::XBEL is not present, return a 5xx response | 
|---|
|  | 169 |  | 
|---|
|  | 170 | my $xbel = XML::XBEL->new; | 
|---|
|  | 171 |  | 
|---|
|  | 172 | $xbel->new_document({ | 
|---|
| [31] | 173 | title => $title, | 
|---|
| [19] | 174 | }); | 
|---|
|  | 175 |  | 
|---|
|  | 176 | for my $bookmark (@resources) { | 
|---|
| [36] | 177 | my $cdatetime = time2isoz $bookmark->ctime; | 
|---|
|  | 178 | my $mdatetime = time2isoz $bookmark->mtime; | 
|---|
| [19] | 179 | # make the timestamps W3C-correct | 
|---|
|  | 180 | s/ /T/ foreach ($cdatetime, $mdatetime); | 
|---|
|  | 181 |  | 
|---|
|  | 182 | $xbel->add_bookmark({ | 
|---|
| [36] | 183 | href     => $bookmark->uri, | 
|---|
|  | 184 | title    => $bookmark->title, | 
|---|
|  | 185 | desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }), | 
|---|
| [19] | 186 | added    => $cdatetime, | 
|---|
|  | 187 | #XXX: are we sure that modified is the mtime of the bookmark or the resource? | 
|---|
|  | 188 | modified => $mdatetime, | 
|---|
|  | 189 | }); | 
|---|
|  | 190 | } | 
|---|
|  | 191 |  | 
|---|
| [59] | 192 | return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]]; | 
|---|
| [36] | 193 | } elsif ($format eq 'text') { | 
|---|
| [59] | 194 | my $text = join '', | 
|---|
| [36] | 195 | map { | 
|---|
|  | 196 | sprintf "# %s\n# Tags: %s\n%s\n", | 
|---|
|  | 197 | $_->title, | 
|---|
|  | 198 | join(', ', @{ $_->tags }), | 
|---|
|  | 199 | $_->uri | 
|---|
|  | 200 | } @resources; | 
|---|
| [59] | 201 | return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]]; | 
|---|
| [49] | 202 | } elsif ($format eq 'csv') { | 
|---|
| [51] | 203 | require Text::CSV::Encoded; | 
|---|
| [59] | 204 | my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' }); | 
|---|
| [49] | 205 | my $text = qq{id,uri,title,tags,ctime,mtime\n}; | 
|---|
|  | 206 | for my $bookmark (@resources) { | 
|---|
|  | 207 | my $success = $csv->combine( | 
|---|
|  | 208 | $bookmark->id, | 
|---|
|  | 209 | $bookmark->uri, | 
|---|
|  | 210 | $bookmark->title, | 
|---|
|  | 211 | join(' ', @{ $bookmark->tags }), | 
|---|
|  | 212 | $bookmark->ctime, | 
|---|
|  | 213 | $bookmark->mtime, | 
|---|
|  | 214 | ); | 
|---|
|  | 215 | $text .= $csv->string . "\n" if $success; | 
|---|
|  | 216 | } | 
|---|
|  | 217 |  | 
|---|
|  | 218 | # include the local timestamp in the attchment filename | 
|---|
|  | 219 | my $dt = time2iso; | 
|---|
|  | 220 | $dt =~ s/[^\d]//g; | 
|---|
|  | 221 |  | 
|---|
| [59] | 222 | my $filename = sprintf( | 
|---|
|  | 223 | 'bookmarks-%s-%s.csv', | 
|---|
|  | 224 | join('_', @tags), | 
|---|
|  | 225 | $dt, | 
|---|
| [49] | 226 | ); | 
|---|
| [59] | 227 |  | 
|---|
|  | 228 | return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]]; | 
|---|
| [5] | 229 | } else { | 
|---|
| [59] | 230 | my $template = Template->new; | 
|---|
| [5] | 231 |  | 
|---|
| [59] | 232 | $template->process( | 
|---|
| [5] | 233 | 'list.tt', | 
|---|
|  | 234 | { | 
|---|
| [59] | 235 | base_url     => $self->base_uri, | 
|---|
| [31] | 236 | title        => $title, | 
|---|
| [52] | 237 | query        => $query, | 
|---|
| [35] | 238 | selected_tag => $tags[0], | 
|---|
| [20] | 239 | search_tags  => \@tags, | 
|---|
| [52] | 240 | links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ], | 
|---|
| [20] | 241 | all_tags     => \@all_tags, | 
|---|
| [5] | 242 | cotags       => \@cotags, | 
|---|
|  | 243 | resources    => \@resources, | 
|---|
|  | 244 | }, | 
|---|
| [59] | 245 | \my $output, | 
|---|
| [5] | 246 | ); | 
|---|
| [59] | 247 | return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; | 
|---|
| [5] | 248 | } | 
|---|
|  | 249 | } | 
|---|
|  | 250 |  | 
|---|
| [9] | 251 | sub feed { | 
|---|
|  | 252 | my $self = shift; | 
|---|
|  | 253 |  | 
|---|
| [59] | 254 | my $query = $self->request->param('q'); | 
|---|
|  | 255 | my @tags = grep { $_ ne '' } $self->request->param('tag'); | 
|---|
| [31] | 256 | my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : ''); | 
|---|
|  | 257 |  | 
|---|
| [33] | 258 | require XML::Atom; | 
|---|
|  | 259 | $XML::Atom::DefaultVersion = "1.0"; | 
|---|
|  | 260 |  | 
|---|
| [9] | 261 | require XML::Atom::Feed; | 
|---|
|  | 262 | require XML::Atom::Entry; | 
|---|
|  | 263 | require XML::Atom::Link; | 
|---|
| [39] | 264 | require XML::Atom::Category; | 
|---|
| [9] | 265 |  | 
|---|
|  | 266 | my $feed = XML::Atom::Feed->new; | 
|---|
| [31] | 267 | $feed->title($title); | 
|---|
| [32] | 268 |  | 
|---|
| [52] | 269 | for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) { | 
|---|
| [38] | 270 | my $atom_link = XML::Atom::Link->new; | 
|---|
|  | 271 | $atom_link->type($link->{type}); | 
|---|
|  | 272 | $atom_link->rel($link->{rel}); | 
|---|
|  | 273 | $atom_link->href($link->{href}->canonical); | 
|---|
|  | 274 | $feed->add_link($atom_link); | 
|---|
|  | 275 | } | 
|---|
| [32] | 276 |  | 
|---|
| [9] | 277 | # construct a feed from the most recent 12 bookmarks | 
|---|
| [59] | 278 | for my $bookmark ($self->bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) { | 
|---|
| [9] | 279 | my $entry = XML::Atom::Entry->new; | 
|---|
| [31] | 280 | $entry->id($bookmark->bookmark_uri->canonical); | 
|---|
|  | 281 | $entry->title($bookmark->title); | 
|---|
| [39] | 282 |  | 
|---|
| [9] | 283 | my $link = XML::Atom::Link->new; | 
|---|
| [31] | 284 | $link->href($bookmark->uri); | 
|---|
| [9] | 285 | $entry->add_link($link); | 
|---|
| [39] | 286 |  | 
|---|
| [31] | 287 | $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags })); | 
|---|
| [39] | 288 |  | 
|---|
|  | 289 | my $cdatetime = time2isoz $bookmark->ctime; | 
|---|
| [33] | 290 | my $mdatetime = time2isoz $bookmark->mtime; | 
|---|
|  | 291 | # make the timestamp W3C-correct | 
|---|
| [39] | 292 | s/ /T/ foreach ($cdatetime, $mdatetime); | 
|---|
|  | 293 | $entry->published($cdatetime); | 
|---|
| [33] | 294 | $entry->updated($mdatetime); | 
|---|
| [39] | 295 |  | 
|---|
|  | 296 | for my $tag (@{ $bookmark->tags }) { | 
|---|
|  | 297 | my $category = XML::Atom::Category->new; | 
|---|
|  | 298 | $category->term($tag); | 
|---|
|  | 299 | $entry->add_category($category); | 
|---|
|  | 300 | } | 
|---|
|  | 301 |  | 
|---|
| [9] | 302 | $feed->add_entry($entry); | 
|---|
|  | 303 | } | 
|---|
|  | 304 |  | 
|---|
| [59] | 305 | return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]]; | 
|---|
| [9] | 306 | } | 
|---|
|  | 307 |  | 
|---|
| [62] | 308 | # returns 1 if there is an If-Modified-Since header and it is newer than the given $mtime | 
|---|
|  | 309 | # returns 0 if there is an If-Modified-Since header but the $mtime is newer | 
|---|
|  | 310 | # returns undef if there is no If-Modified-Since header | 
|---|
| [67] | 311 | sub _request_is_newer_than { | 
|---|
| [62] | 312 | my $self = shift; | 
|---|
|  | 313 | my $mtime = shift; | 
|---|
|  | 314 |  | 
|---|
|  | 315 | # check If-Modified-Since header to return cache response | 
|---|
|  | 316 | if ($self->request->env->{HTTP_IF_MODIFIED_SINCE}) { | 
|---|
|  | 317 | my $cache_time = str2time($self->request->env->{HTTP_IF_MODIFIED_SINCE}); | 
|---|
|  | 318 | return $mtime <= $cache_time ? 1 : 0; | 
|---|
|  | 319 | } else { | 
|---|
|  | 320 | return; | 
|---|
|  | 321 | } | 
|---|
|  | 322 | } | 
|---|
|  | 323 |  | 
|---|
| [5] | 324 | sub view { | 
|---|
| [59] | 325 | my ($self, $id) = @_; | 
|---|
| [5] | 326 |  | 
|---|
| [59] | 327 | my $format = $self->request->param('format') || 'html'; | 
|---|
|  | 328 |  | 
|---|
|  | 329 | my $bookmark = $self->get_bookmark({ id => $id }); | 
|---|
| [5] | 330 | if ($bookmark) { | 
|---|
| [67] | 331 | return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime); | 
|---|
| [62] | 332 |  | 
|---|
| [59] | 333 | my $last_modified = time2str($bookmark->mtime); | 
|---|
| [57] | 334 |  | 
|---|
| [30] | 335 | if ($format eq 'json') { | 
|---|
| [59] | 336 | my $json = decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark)); | 
|---|
|  | 337 | return [200, ['Content-Type' => 'application/json; charset=UTF-8', 'Last-Modified' => $last_modified], [$json]]; | 
|---|
| [5] | 338 | } else { | 
|---|
| [30] | 339 | # display the bookmark form for this bookmark | 
|---|
| [59] | 340 | my $template = Template->new; | 
|---|
|  | 341 | $template->process( | 
|---|
| [30] | 342 | 'bookmark.tt', | 
|---|
| [66] | 343 | { bookmark => $bookmark }, | 
|---|
| [59] | 344 | \my $output, | 
|---|
| [30] | 345 | ); | 
|---|
| [59] | 346 | return [200, ['Content-Type' => 'text/html; charset=UTF-8', 'Last-Modified' => $last_modified], [$output]]; | 
|---|
| [30] | 347 | } | 
|---|
|  | 348 | } else { | 
|---|
| [59] | 349 | return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]]; | 
|---|
| [30] | 350 | } | 
|---|
|  | 351 | } | 
|---|
|  | 352 |  | 
|---|
|  | 353 | sub view_field { | 
|---|
| [59] | 354 | my ($self, $id, $field) = @_; | 
|---|
| [30] | 355 |  | 
|---|
| [62] | 356 | my $bookmark = $self->get_bookmark({ id => $id }); | 
|---|
| [30] | 357 | if ($bookmark) { | 
|---|
| [67] | 358 | return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime); | 
|---|
| [62] | 359 |  | 
|---|
| [63] | 360 | # check whether the requested field is part of the bookmark | 
|---|
|  | 361 | if (!$bookmark->meta->has_attribute($field)) { | 
|---|
|  | 362 | return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$field" is not a valid bookmark data field}]]; | 
|---|
| [5] | 363 | } | 
|---|
| [63] | 364 |  | 
|---|
|  | 365 | # respond with just the requested field as plain text | 
|---|
|  | 366 | my $value = $bookmark->$field; | 
|---|
| [62] | 367 | my $last_modified = time2str($bookmark->mtime); | 
|---|
|  | 368 | return [200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Last-Modified' => $last_modified], [ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value]]; | 
|---|
| [5] | 369 | } else { | 
|---|
| [59] | 370 | return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]]; | 
|---|
| [5] | 371 | } | 
|---|
|  | 372 | } | 
|---|
|  | 373 |  | 
|---|
| [63] | 374 | sub create_and_redirect { | 
|---|
| [5] | 375 | my $self = shift; | 
|---|
| [59] | 376 |  | 
|---|
|  | 377 | my $uri   = $self->request->param('uri'); | 
|---|
|  | 378 | my $title = $self->request->param('title'); | 
|---|
|  | 379 | my @tags  = split ' ', $self->request->param('tags'); | 
|---|
|  | 380 |  | 
|---|
|  | 381 | my $bookmark = $self->bookmarks->add({ | 
|---|
| [5] | 382 | uri   => $uri, | 
|---|
|  | 383 | title => $title, | 
|---|
|  | 384 | tags  => \@tags, | 
|---|
|  | 385 | }); | 
|---|
|  | 386 |  | 
|---|
| [59] | 387 | #TODO: not RESTful; the proper RESTful response would be a 201 | 
|---|
|  | 388 | return [303, ['Location' => $bookmark->bookmark_uri->canonical], []]; | 
|---|
| [5] | 389 | } | 
|---|
|  | 390 |  | 
|---|
| [63] | 391 | sub update_and_redirect { | 
|---|
| [45] | 392 | my $self = shift; | 
|---|
| [59] | 393 | my $id = shift; | 
|---|
| [45] | 394 |  | 
|---|
| [59] | 395 | my $bookmark = $self->bookmarks->get_bookmark({ id => $id }); | 
|---|
| [45] | 396 | if ($bookmark) { | 
|---|
|  | 397 | # update the URI, title, and tags | 
|---|
| [59] | 398 | $bookmark->uri($self->request->param('uri')); | 
|---|
|  | 399 | $bookmark->title($self->request->param('title')); | 
|---|
|  | 400 | $bookmark->tags([ split ' ', $self->request->param('tags') || '' ]); | 
|---|
| [45] | 401 |  | 
|---|
| [46] | 402 | # write to the database | 
|---|
| [59] | 403 | $self->bookmarks->update($bookmark); | 
|---|
| [46] | 404 |  | 
|---|
| [59] | 405 | #TODO: not RESTful; proper response would be a 200 | 
|---|
|  | 406 | return [303, ['Location' => $bookmark->bookmark_uri->canonical], []]; | 
|---|
| [45] | 407 | } else { | 
|---|
| [59] | 408 | return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]]; | 
|---|
| [45] | 409 | } | 
|---|
|  | 410 | } | 
|---|
|  | 411 |  | 
|---|
| [5] | 412 | 1; | 
|---|