File Coverage

blib/lib/Yancy/Controller/Yancy.pm
Criterion Covered Total %
statement 252 257 98.0
branch 79 88 89.7
condition 70 93 75.2
subroutine 25 25 100.0
pod 8 8 100.0
total 434 471 92.1


>>.
line stmt bran cond sub pod time code
1             package Yancy::Controller::Yancy;
2             our $VERSION = '1.086';
3             # ABSTRACT: Basic controller for displaying content
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod plugin Yancy => {
9             #pod schema => {
10             #pod blog => {
11             #pod properties => {
12             #pod id => { type => 'integer' },
13             #pod title => { type => 'string' },
14             #pod html => { type => 'string' },
15             #pod },
16             #pod },
17             #pod },
18             #pod };
19             #pod
20             #pod app->routes->get( '/' )->to(
21             #pod 'yancy#list',
22             #pod schema => 'blog',
23             #pod template => 'index',
24             #pod );
25             #pod
26             #pod __DATA__
27             #pod @@ index.html.ep
28             #pod % for my $item ( @{ stash 'items' } ) {
29             #pod

<%= $item->{title} %>

30             #pod <%== $item->{html} %>
31             #pod % }
32             #pod
33             #pod =head1 DESCRIPTION
34             #pod
35             #pod This controller contains basic route handlers for displaying content
36             #pod configured in Yancy schema. These route handlers reduce the amount
37             #pod of code you need to write to display or modify your content.
38             #pod
39             #pod Route handlers use the Mojolicious C for configuration. These values
40             #pod can be set at route creation, or by an C route handler.
41             #pod
42             #pod Using these route handlers also gives you a built-in JSON API for your
43             #pod website. Any user agent that requests JSON will get JSON instead of
44             #pod HTML. For full details on how JSON clients are detected, see
45             #pod L.
46             #pod
47             #pod =head1 ACTION HOOKS
48             #pod
49             #pod Every action can call one or more of your application's
50             #pod L.
51             #pod These helpers can change the item before it is displayed or
52             #pod before it is saved to the database.
53             #pod
54             #pod These helpers get one argument: An item being displayed, created, saved,
55             #pod or deleted. The helper then returns the item to be displayed, created,
56             #pod or saved.
57             #pod
58             #pod use Mojolicious::Lite -signatures;
59             #pod plugin Yancy => { ... };
60             #pod
61             #pod # Set a last_updated timestamp when creating or updating events
62             #pod helper update_timestamp => sub( $c, $item ) {
63             #pod $item->{last_updated} = time;
64             #pod return $item;
65             #pod };
66             #pod post '/event/:event_id' => 'yancy#set',
67             #pod {
68             #pod event_id => undef,
69             #pod schema => 'events',
70             #pod helpers => [ 'update_timestamp' ],
71             #pod forward_to => 'events.get',
72             #pod },
73             #pod 'events.set';
74             #pod
75             #pod Helpers can also be anonymous subrefs for those times when you want a
76             #pod unique behavior for a single route.
77             #pod
78             #pod # Format the last_updated timestamp when showing event details
79             #pod use Time::Piece;
80             #pod get '/event/:event_id' => 'yancy#get',
81             #pod {
82             #pod schema => 'events',
83             #pod helpers => [
84             #pod sub( $c, $item ) {
85             #pod $item->{last_updated} = Time::Piece->new( $item->{last_updated} );
86             #pod return $item;
87             #pod },
88             #pod ],
89             #pod },
90             #pod 'events.get';
91             #pod
92             #pod =head1 EXTENDING
93             #pod
94             #pod Here are some tips for inheriting from this controller to add
95             #pod functionality.
96             #pod
97             #pod =over
98             #pod
99             #pod =item set
100             #pod
101             #pod =over
102             #pod
103             #pod =item *
104             #pod
105             #pod When setting field values to add to the updated/created item, use C<<
106             #pod $c->req->param >> not C<< $c->param >>. The underlying code uses C<<
107             #pod $c->req->param >> to get all of the params, which will not be updated if
108             #pod you use C<< $c->param >>.
109             #pod
110             #pod =back
111             #pod
112             #pod =back
113             #pod
114             #pod =head1 DIAGNOSTICS
115             #pod
116             #pod =over
117             #pod
118             #pod =item Page not found
119             #pod
120             #pod If you get a C<404 Not Found> response or Mojolicious's "Page not found... yet!" page,
121             #pod it could be from one of a few reasons:
122             #pod
123             #pod =over
124             #pod
125             #pod =item No route with the given path was found
126             #pod
127             #pod Check to make sure that your routes match the URL.
128             #pod
129             #pod =item Configured template not found
130             #pod
131             #pod Make sure the template is configured and named correctly and the correct format
132             #pod and renderer are being used.
133             #pod
134             #pod =back
135             #pod
136             #pod The Mojolicious debug log will have more information. Make sure you are
137             #pod logging at C level by running in C mode (the
138             #pod default), or setting the C environment variable to
139             #pod C. See L
140             #pod tutorial|Mojolicious::Guides::Tutorial/Mode> for more information.
141             #pod
142             #pod =back
143             #pod
144             #pod =head1 TEMPLATES
145             #pod
146             #pod To override these templates, add your own at the designated path inside
147             #pod your app's C directory.
148             #pod
149             #pod =head2 yancy/table.html.ep
150             #pod
151             #pod The default C template. Uses the following additional stash values
152             #pod for configuration:
153             #pod
154             #pod =over
155             #pod
156             #pod =item properties
157             #pod
158             #pod An array reference of columns to display in the table. The same as
159             #pod C in the schema configuration. Defaults to
160             #pod C in the schema configuration or all of the schema's
161             #pod columns in C order. See L
162             #pod Your Schema> for more information.
163             #pod
164             #pod =item table
165             #pod
166             #pod get '/events' => (
167             #pod controller => 'yancy',
168             #pod action => 'list',
169             #pod table => {
170             #pod thead => 0, # Disable column headers
171             #pod class => 'table table-responsive', # Add a class
172             #pod },
173             #pod );
174             #pod
175             #pod Attributes for the table tag. A hash reference of the following keys:
176             #pod
177             #pod =over
178             #pod
179             #pod =item thead
180             #pod
181             #pod Whether or not to display the table head section, which contains the
182             #pod column headings. Defaults to true (C<1>). Set to false (C<0>) to
183             #pod disable C<<
184             #pod
185             #pod =item show_filter
186             #pod
187             #pod Show filter input boxes for each column in the header. Pressing C
188             #pod will filter the table.
189             #pod
190             #pod =item id
191             #pod
192             #pod The ID of the table element.
193             #pod
194             #pod =item class
195             #pod
196             #pod The class(s) of the table element.
197             #pod
198             #pod =back
199             #pod
200             #pod =back
201             #pod
202             #pod =head1 SEE ALSO
203             #pod
204             #pod L
205             #pod
206             #pod =cut
207              
208 7     7   44300 use Mojo::Base 'Mojolicious::Controller';
  7         18  
  7         66  
209 7     7   20512 use Mojo::JSON qw( to_json );
  7         16  
  7         397  
210 7     7   42 use Yancy::Util qw( derp is_type );
  7         15  
  7         317  
211 7     7   48 use POSIX qw( ceil );
  7         17  
  7         61  
212              
213             #pod =method schema
214             #pod
215             #pod Get the L object to handle the current request. This uses
216             #pod the C stash value to look up the schema from the default model. Override
217             #pod the default model using the C stash value.
218             #pod
219             #pod =cut
220              
221             sub schema {
222 538     538 1 1045 my ( $self ) = @_;
223 538 50       1404 if ( $self->stash( 'collection' ) ) {
224 0         0 derp '"collection" stash key is now "schema" in controller configuration';
225             }
226 538   100     5905 my $schema_name = $self->stash( 'schema' ) || $self->stash( 'collection' )
227             || die "Schema name not defined in stash";
228 532   66     5340 my $model = $self->stash( 'model' ) // $self->yancy->model;
229 532         3604 return $model->schema( $schema_name );
230             }
231              
232             #pod =method item_id
233             #pod
234             #pod Get the ID for the currently-requested item, if available, or C.
235             #pod
236             #pod =cut
237              
238             sub item_id {
239 155     155 1 376 my ( $self ) = @_;
240 155         514 my $id_field = $self->schema->id_field;
241 155 100       1412 if ( ref $id_field eq 'ARRAY' ) {
242 7         29 my $id = { map { $_ => $self->stash( $_ ) } grep defined $self->stash( $_ ), @$id_field };
  10         148  
243 7 100       118 return keys %$id == @$id_field ? $id : undef;
244             }
245 148   100     423 return $self->stash( $id_field ) // undef;
246             }
247              
248             #pod =method clean_item
249             #pod
250             #pod Clean the given item by removing any sensitive fields (like passwords).
251             #pod
252             #pod =cut
253              
254             sub clean_item {
255 170     170 1 1297 my ( $self, $item ) = @_;
256 170         451 my $props = $self->schema->json_schema->{properties};
257 170   100     1186 my @keep_props = grep { ($props->{$_}{format}//'') ne 'password' } keys %$props;
  1250         4267  
258 170         465 return { map { $_ => $item->{$_} } @keep_props };
  1216         2816  
259             }
260              
261             #pod =method list
262             #pod
263             #pod $routes->get( '/' )->to(
264             #pod 'yancy#list',
265             #pod schema => $schema_name,
266             #pod template => $template_name,
267             #pod );
268             #pod
269             #pod This method is used to list content.
270             #pod
271             #pod =head4 Input Stash
272             #pod
273             #pod This method uses the following stash values for configuration:
274             #pod
275             #pod =over
276             #pod
277             #pod =item schema
278             #pod
279             #pod The schema to use. Required.
280             #pod
281             #pod =item template
282             #pod
283             #pod The name of the template to use. See L
284             #pod for how template names are resolved. Defaults to C.
285             #pod
286             #pod =item limit
287             #pod
288             #pod The number of items to show on the page. Defaults to C<10>.
289             #pod
290             #pod =item page
291             #pod
292             #pod The page number to show. Defaults to C<1>. The page number will
293             #pod be used to calculate the C parameter to L.
294             #pod
295             #pod =item filter
296             #pod
297             #pod A hash reference of field/value pairs to filter the contents of the list
298             #pod or a subref that generates this hash reference. The subref will be passed
299             #pod the current controller object (C<$c>).
300             #pod
301             #pod This overrides any query filters and so can be used to enforce
302             #pod authorization / security.
303             #pod
304             #pod =item order_by
305             #pod
306             #pod Set the default order for the items. Supports any L
307             #pod C structure.
308             #pod
309             #pod =item before_render
310             #pod
311             #pod An array reference of hooks to call once for each item in the C list.
312             #pod See L for usage.
313             #pod
314             #pod =back
315             #pod
316             #pod =head4 Output Stash
317             #pod
318             #pod The following stash values are set by this method:
319             #pod
320             #pod =over
321             #pod
322             #pod =item items
323             #pod
324             #pod An array reference of items to display.
325             #pod
326             #pod =item total
327             #pod
328             #pod The total number of items that match the given filters.
329             #pod
330             #pod =item total_pages
331             #pod
332             #pod The number of pages of items. Can be used for pagination.
333             #pod
334             #pod =back
335             #pod
336             #pod =head4 Query Params
337             #pod
338             #pod The following URL query parameters are allowed for this method:
339             #pod
340             #pod =over
341             #pod
342             #pod =item $page
343             #pod
344             #pod Instead of using the C stash value, you can use the C<$page> query
345             #pod paremeter to set the page.
346             #pod
347             #pod =item $offset
348             #pod
349             #pod Instead of using the C stash value, you can use the C<$offset>
350             #pod query parameter to set the page offset. This is overridden by the
351             #pod C<$page> query parameter.
352             #pod
353             #pod =item $limit
354             #pod
355             #pod Instead of using the C stash value, you can use the C<$limit>
356             #pod query parameter to allow users to specify their own page size.
357             #pod
358             #pod =item $order_by
359             #pod
360             #pod One or more fields to order by. Can be specified as C<< >> or
361             #pod C<< asc: >> to sort in ascending order or C<< desc: >>
362             #pod to sort in descending order.
363             #pod
364             #pod =item $match
365             #pod
366             #pod How to match multiple field filters. Can be C or C (default
367             #pod C). C means all fields must match for a row to be returned.
368             #pod C means at least one field must match for a row to be returned.
369             #pod
370             #pod =item Additional Field Filters
371             #pod
372             #pod Any named query parameter that matches a field in the schema will be
373             #pod used to further filter the results. The stash C will override
374             #pod this filter, so that the stash C can be used for security.
375             #pod
376             #pod =back
377             #pod
378             #pod =head4 Content Negotiation
379             #pod
380             #pod If the C request accepts content type is C, or
381             #pod the URL ends in C<.json>, the results page will be returned as a JSON
382             #pod object with the following keys:
383             #pod
384             #pod =over
385             #pod
386             #pod =item items
387             #pod
388             #pod The array of items for this page.
389             #pod
390             #pod =item total
391             #pod
392             #pod The total number of results for the query.
393             #pod
394             #pod =item offset
395             #pod
396             #pod The current offset. Get the next page of results by increasing this
397             #pod number and setting the C<$offset> query parameter.
398             #pod
399             #pod =back
400             #pod
401             #pod =cut
402              
403             sub list {
404 53     53 1 665540 my ( $c ) = @_;
405 53         250 my ( $filter, $opt ) = $c->_get_list_args;
406 51         149 my $result = $c->schema->list( $filter, $opt );
407              
408             # XXX: Filters are deprecated
409 51   50     1046 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
410             || die "Schema name not defined in stash";
411             $result->{items} = [
412             map {
413 91         336 $c->yancy->filter->apply( $schema_name, $_, 'x-filter-output' )
414             }
415 51         689 @{ $result->{items} }
  51         126  
416             ];
417              
418 51   100     131 for my $helper ( @{ $c->stash( 'before_render' ) // [] } ) {
  51         205  
419 2         47 $c->$helper( $_ ) for @{ $result->{items} };
  2         10  
420             }
421 51         752 $result->{items} = [ map $c->clean_item( $_ ), @{ $result->{items} } ];
  51         211  
422             # By the time `any` is reached, the format will be blank. To support
423             # any format of template, we need to restore the format stash
424 51         344 my $format = $c->stash( 'format' );
425             return $c->respond_to(
426             json => sub {
427 33     33   15938 $c->stash( json => { %$result, offset => $opt->{offset} } );
428             },
429             any => sub {
430 18 100   18   9171 if ( !$c->stash( 'template' ) ) {
431 4         50 $c->stash( template => 'yancy/table' );
432             }
433             $c->stash(
434             ( format => $format )x!!$format,
435             %$result,
436 18         370 total_pages => ceil( $result->{total} / $opt->{limit} ),
437             );
438             },
439 51         883 );
440             }
441              
442             #pod =method get
443             #pod
444             #pod $routes->get( '/:id_field' )->to(
445             #pod 'yancy#get',
446             #pod schema => $schema_name,
447             #pod template => $template_name,
448             #pod );
449             #pod
450             #pod This method is used to show a single item.
451             #pod
452             #pod =head4 Input Stash
453             #pod
454             #pod This method uses the following stash values for configuration:
455             #pod
456             #pod =over
457             #pod
458             #pod =item schema
459             #pod
460             #pod The schema to use. Required.
461             #pod
462             #pod =item "id_field"
463             #pod
464             #pod The ID field(s) for the item should be defined as stash items, usually via
465             #pod route placeholders named after the field.
466             #pod
467             #pod # Schema ID field is "page_id"
468             #pod $routes->get( '/pages/:page_id' )
469             #pod
470             #pod =item template
471             #pod
472             #pod The name of the template to use. See L
473             #pod for how template names are resolved.
474             #pod
475             #pod =item before_render
476             #pod
477             #pod An array reference of helpers to call before the item is displayed. See
478             #pod L for usage.
479             #pod
480             #pod =back
481             #pod
482             #pod =head4 Output Stash
483             #pod
484             #pod The following stash values are set by this method:
485             #pod
486             #pod =over
487             #pod
488             #pod =item item
489             #pod
490             #pod The item that is being displayed.
491             #pod
492             #pod =back
493             #pod
494             #pod =head4 Content Negotiation
495             #pod
496             #pod If the C request accepts content type is C, or
497             #pod the URL ends in C<.json>, the item will be returned as a JSON object.
498             #pod
499             #pod =cut
500              
501             sub get {
502 27     27 1 501763 my ( $c ) = @_;
503 27         118 my $schema = $c->schema;
504 26         114 my $id = $c->item_id;
505 26 100       314 if ( !$id ) {
506 1         7 my $id_field = $schema->id_field;
507 1 50       29 die sprintf "ID field(s) %s not defined in stash",
508             join ', ', map qq("$_"), $id_field eq 'ARRAY' ? @$id_field : $id_field;
509             }
510 25         99 my $item = $schema->get( $id );
511 25 100       492 if ( !$item ) {
512 2         31 $c->reply->not_found;
513 2         293435 return;
514             }
515              
516             # XXX: Filters are deprecated
517 23   50     97 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
518             || die "Schema name not defined in stash";
519 23         353 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
520              
521 23   100     73 for my $helper ( @{ $c->stash( 'before_render' ) // [] } ) {
  23         101  
522 2         43 $c->$helper( $item );
523             }
524              
525 23         399 $item = $c->clean_item( $item );
526              
527             # By the time `any` is reached, the format will be blank. To support
528             # any format of template, we need to restore the format stash
529 23         104 my $format = $c->stash( 'format' );
530             return $c->respond_to(
531 18     18   11096 json => sub { $c->stash( json => $item ) },
532 5     5   3193 any => sub { $c->stash( item => $item, ( format => $format )x!!$format ) },
533 23         426 );
534             }
535              
536             #pod =method set
537             #pod
538             #pod # Update an existing item
539             #pod $routes->any( [ 'GET', 'POST' ] => '/:id_field/edit' )->to(
540             #pod 'yancy#set',
541             #pod schema => $schema_name,
542             #pod template => $template_name,
543             #pod );
544             #pod
545             #pod # Create a new item
546             #pod $routes->any( [ 'GET', 'POST' ] => '/create' )->to(
547             #pod 'yancy#set',
548             #pod schema => $schema_name,
549             #pod template => $template_name,
550             #pod forward_to => $route_name,
551             #pod );
552             #pod
553             #pod This route creates a new item or updates an existing item in
554             #pod a schema. If the user is making a C request, they will simply
555             #pod be shown the template. If the user is making a C or C
556             #pod request, the form parameters will be read, the data will be validated
557             #pod against L,
558             #pod and the user will either be shown the form again with the
559             #pod result of the form submission (success or failure) or the user will be
560             #pod forwarded to another place.
561             #pod
562             #pod Displaying a form could be done as a separate route using the C
563             #pod method, but with more code:
564             #pod
565             #pod $routes->get( '/:id_field/edit' )->to(
566             #pod 'yancy#get',
567             #pod schema => $schema_name,
568             #pod template => $template_name,
569             #pod );
570             #pod $routes->post( '/:id_field/edit' )->to(
571             #pod 'yancy#set',
572             #pod schema => $schema_name,
573             #pod template => $template_name,
574             #pod );
575             #pod
576             #pod =head4 Input Stash
577             #pod
578             #pod This method uses the following stash values for configuration:
579             #pod
580             #pod =over
581             #pod
582             #pod =item schema
583             #pod
584             #pod The schema to use. Required.
585             #pod
586             #pod =item "id_field"
587             #pod
588             #pod The ID field(s) for the item should be defined as stash items, usually via
589             #pod route placeholders named after the field. Optional: If not specified, a new
590             #pod item will be created.
591             #pod
592             #pod # Schema ID field is "page_id"
593             #pod $routes->post( '/pages/:page_id' )
594             #pod
595             #pod =item template
596             #pod
597             #pod The name of the template to use. See L
598             #pod for how template names are resolved.
599             #pod
600             #pod =item before_write
601             #pod
602             #pod An array reference of helpers to call after the new values are applied
603             #pod to the item, but before the item is written to the database. See
604             #pod L for usage.
605             #pod
606             #pod =item forward_to
607             #pod
608             #pod The name of a route to forward the user to on success. Optional. Any
609             #pod route placeholders that match item field names will be filled in.
610             #pod
611             #pod $routes->get( '/:blog_id/:slug' )->name( 'blog.view' );
612             #pod $routes->post( '/create' )->to(
613             #pod 'yancy#set',
614             #pod schema => 'blog',
615             #pod template => 'blog_edit.html.ep',
616             #pod forward_to => 'blog.view',
617             #pod );
618             #pod
619             #pod # { id => 1, slug => 'first-post' }
620             #pod # forward_to => '/1/first-post'
621             #pod
622             #pod Forwarding will not happen for JSON requests.
623             #pod
624             #pod =item properties
625             #pod
626             #pod Restrict this route to only setting the given properties. An array
627             #pod reference of properties to allow. Trying to set additional properties
628             #pod will result in an error.
629             #pod
630             #pod B Unless restricted to certain properties using this
631             #pod configuration, this method accepts all valid data configured for the
632             #pod schema. The data being submitted can be more than just the fields
633             #pod you make available in the form. If you do not want certain data to be
634             #pod written through this form, you can prevent it by using this.
635             #pod
636             #pod =back
637             #pod
638             #pod =head4 Output Stash
639             #pod
640             #pod The following stash values are set by this method:
641             #pod
642             #pod =over
643             #pod
644             #pod =item item
645             #pod
646             #pod The item that is being edited, if the C is given. Otherwise, the
647             #pod item that was created.
648             #pod
649             #pod =item errors
650             #pod
651             #pod An array of hash references of errors that occurred during data
652             #pod validation. Each hash reference is either a L
653             #pod object or a hash reference with a C field. See L
654             #pod yancy.validate helper docs|Mojolicious::Plugin::Yancy/yancy.validate>
655             #pod and L for more details.
656             #pod
657             #pod =back
658             #pod
659             #pod =head4 Query Params
660             #pod
661             #pod This method accepts query parameters named for the fields in the schema.
662             #pod
663             #pod Each field in the item is also set as a param using
664             #pod L so that tag helpers like C
665             #pod will be pre-filled with the values. See
666             #pod L for more information. This also means
667             #pod that fields can be pre-filled with initial data or new data by using GET
668             #pod query parameters.
669             #pod
670             #pod =head4 CSRF Protection
671             #pod
672             #pod This method is protected by L
673             #pod (CSRF) protection|Mojolicious::Guides::Rendering/Cross-site request
674             #pod forgery>. CSRF protection prevents other sites from tricking your users
675             #pod into doing something on your site that they didn't intend, such as
676             #pod editing or deleting content. You must add a C<< <%= csrf_field %> >> to
677             #pod your form in order to delete an item successfully. See
678             #pod L.
679             #pod
680             #pod =head4 Content Negotiation
681             #pod
682             #pod If the C or C request content type is C,
683             #pod the request body will be treated as a JSON object to create/set. In this
684             #pod case, the form query parameters are not used.
685             #pod
686             #pod =cut
687              
688             sub set {
689 57     57 1 739029 my ( $c ) = @_;
690 57         284 my $schema = $c->schema;
691 55         309 my $id_field = $schema->id_field;
692 55         714 my $id = $c->item_id;
693              
694             # Display the form, if requested. This makes the simple case of
695             # displaying and managing a form easier with a single route instead
696             # of two routes (one to "yancy#get" and one to "yancy#set")
697 55 100       905 if ( $c->req->method eq 'GET' ) {
698 13 100       262 if ( defined $id ) {
699 9         52 my $item = $schema->get( $id );
700              
701             # XXX: Filters are deprecated
702 9   50     224 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
703             || die "Schema name not defined in stash";
704 9         166 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
705              
706 9         54 $c->stash( item => $c->clean_item( $item ) );
707 9         216 my $props = $schema->json_schema->{properties};
708 9         78 for my $key ( keys %$props ) {
709             # Mojolicious TagHelpers take current values through the
710             # params, but also we allow pre-filling values through the
711             # GET query parameters (except for passwords)
712             next if $props->{ $key }{ format }
713 77 100 100     1541 && $props->{ $key }{ format } eq 'password';
714 72   100     201 $c->param( $key => $c->param( $key ) // $item->{ $key } );
715             }
716             }
717             else {
718             # Add an empty hashref for creating a new item
719 4         19 $c->stash( item => {} );
720             }
721              
722 13         458 $c->respond_to(
723             json => {
724             status => 400,
725             json => {
726             errors => [
727             {
728             message => 'GET request for JSON invalid',
729             },
730             ],
731             },
732             },
733             any => { },
734             );
735 13         89033 return;
736             }
737              
738 42 100 100     916 if ( $c->accepts( 'html' ) && $c->validation->csrf_protect->has_error( 'csrf_token' ) ) {
739 4         6718 $c->app->log->error( 'CSRF token validation failed' );
740 4         107 my $item = $schema->get( $id );
741              
742             # XXX: Filters are deprecated
743 4   50     60 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
744             || die "Schema name not defined in stash";
745 4         74 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
746              
747 4         28 $c->render(
748             status => 400,
749             item => $c->clean_item( $item ),
750             errors => [
751             {
752             message => 'CSRF token invalid.',
753             },
754             ],
755             );
756 4         17069 return;
757             }
758              
759 38   66     39787 my $data = eval { $c->req->json } || $c->req->params->to_hash;
760 38         30918 delete $data->{csrf_token};
761 38         102 my @errors;
762              
763 38         173 my $allowed_props = $c->stash( 'properties' );
764 38         557 my $props = $schema->json_schema->{properties};
765 38         426 for my $key ( keys %$props ) {
766 305 100 100     2165 if ( $allowed_props && $data->{ $key } && !grep { $_ eq $key } @$allowed_props ) {
  10   100     34  
767 1         11 push @errors, {message => sprintf( 'Properties not allowed: %s.', $key ), path => '/'};
768             }
769 305   100     1001 my $format = $props->{ $key }{ format } // '';
770             # Password cannot be changed to an empty string
771 305 100 100     1053 if ( $format eq 'password' ) {
    100          
772 16 100 66     151 if ( exists $data->{ $key } &&
      100        
773             ( !defined $data->{ $key } || $data->{ $key } eq '' )
774             ) {
775 1         4 delete $data->{ $key };
776             }
777             }
778             # Upload files
779             elsif ( $format eq 'filepath' and my $upload = $c->param( $key ) ) {
780 1         46 my $path = $c->yancy->file->write( $upload );
781 1         139 $data->{ $key } = $path;
782             }
783             }
784 38 100       181 if ( @errors ) {
785 1         6 $c->res->code( 400 );
786 1         21 my $item = $schema->get( $id );
787              
788             # XXX: Filters are deprecated
789 1   50     22 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
790             || die "Schema name not defined in stash";
791 1         18 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
792              
793 1         13 $c->respond_to(
794             json => { json => { errors => \@errors } },
795             any => { item => $item, errors => \@errors },
796             );
797 1         2641 return;
798             }
799              
800 37   100     200 for my $helper ( @{ $c->stash( 'before_write' ) // [] } ) {
  37         148  
801 5         78 $c->$helper( $data );
802             }
803             # ID could change during our helpers
804 37         592 $id = $c->item_id;
805 37         486 my $has_id = defined $id;
806              
807             # XXX: Filters are deprecated
808 37   50     118 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
809             || die "Schema name not defined in stash";
810 37         472 $data = $c->yancy->filter->apply( $schema_name, $data );
811              
812 37 100       175 if ( $has_id ) {
813 23         59 eval { $schema->set( $id, $data ) };
  23         132  
814             # ID field(s) may have changed
815 23 100       114 if ( ref $id_field eq 'ARRAY' ) {
816 1         4 for my $field ( @$id_field ) {
817 2   33     11 $id->{ $field } = $data->{ $field } || $id->{ $field };
818             }
819             }
820             else {
821 22   33     269 $id = $data->{ $id_field } || $id;
822             }
823             #; $c->app->log->info( 'Set success, new id: ' . $id );
824             }
825             else {
826 14         37 $id = eval { $schema->create( $data ) };
  14         83  
827             }
828              
829 37 100       195 if ( my $errors = $@ ) {
830 3 100       27 if ( ref $errors eq 'ARRAY' ) {
831             # Validation error
832 1         6 $c->res->code( 400 );
833 1         23 $errors = [map {{message => $_->message, path => $_->path }} @$errors];
  2         16  
834             }
835             else {
836             # Unknown error
837 2         41 $c->res->code( 500 );
838 2         51 $errors = [ { message => $errors } ];
839             }
840 3         25 my $item = $c->clean_item( $schema->get( $id ) );
841              
842             # XXX: Filters are deprecated
843 3   50     17 my $schema_name = $c->stash( 'schema' ) || $c->stash( 'collection' )
844             || die "Schema name not defined in stash";
845 3         62 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
846              
847 3         32 $c->respond_to(
848             json => { json => { errors => $errors } },
849             any => { item => $item, errors => $errors },
850             );
851 3         12263 return;
852             }
853              
854 34         154 my $item = $c->clean_item( $schema->get( $id ) );
855             # XXX: Filters are deprecated
856 34         236 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
857              
858             return $c->respond_to(
859             json => sub {
860 24 100   24   11987 $c->stash(
861             status => $has_id ? 200 : 201,
862             json => $item,
863             );
864             },
865             any => sub {
866 10 100   10   5544 if ( my $route = $c->stash( 'forward_to' ) ) {
867 6         157 $c->redirect_to( $route, %$item );
868 6         9938 return;
869             }
870 4         95 $c->stash( item => $item );
871             },
872 34         472 );
873             }
874              
875             #pod =method delete
876             #pod
877             #pod $routes->any( [ 'GET', 'POST' ], '/delete/:id_field' )->to(
878             #pod 'yancy#delete',
879             #pod schema => $schema_name,
880             #pod template => $template_name,
881             #pod forward_to => $route_name,
882             #pod );
883             #pod
884             #pod This route deletes an item from a schema. If the user is making
885             #pod a C request, they will simply be shown the template (which can be
886             #pod used to confirm the delete). If the user is making a C or C
887             #pod request, the item will be deleted and the user will either be shown the
888             #pod form again with the result of the form submission (success or failure)
889             #pod or the user will be forwarded to another place.
890             #pod
891             #pod =head4 Input Stash
892             #pod
893             #pod This method uses the following stash values for configuration:
894             #pod
895             #pod =over
896             #pod
897             #pod =item schema
898             #pod
899             #pod The schema to use. Required.
900             #pod
901             #pod =item "id_field"
902             #pod
903             #pod The ID field(s) for the item should be defined as stash items, usually via
904             #pod route placeholders named after the field.
905             #pod
906             #pod # Schema ID field is "page_id"
907             #pod $routes->get( '/pages/:page_id' )
908             #pod
909             #pod =item template
910             #pod
911             #pod The name of the template to use. See L
912             #pod for how template names are resolved.
913             #pod
914             #pod =item forward_to
915             #pod
916             #pod The name of a route to forward the user to on success. Optional.
917             #pod Forwarding will not happen for JSON requests.
918             #pod
919             #pod =item before_delete
920             #pod
921             #pod An array reference of helpers to call just before the item is deleted.
922             #pod See L for usage.
923             #pod
924             #pod =back
925             #pod
926             #pod =head4 Output Stash
927             #pod
928             #pod The following stash values are set by this method:
929             #pod
930             #pod =over
931             #pod
932             #pod =item item
933             #pod
934             #pod The item that will be deleted. If displaying the form again after the item is deleted,
935             #pod this will be C.
936             #pod
937             #pod =back
938             #pod
939             #pod =head4 CSRF Protection
940             #pod
941             #pod This method is protected by L
942             #pod (CSRF) protection|Mojolicious::Guides::Rendering/Cross-site request
943             #pod forgery>. CSRF protection prevents other sites from tricking your users
944             #pod into doing something on your site that they didn't intend, such as
945             #pod editing or deleting content. You must add a C<< <%= csrf_field %> >> to
946             #pod your form in order to delete an item successfully. See
947             #pod L.
948             #pod
949             #pod =cut
950              
951             sub delete {
952 23     23 1 506307 my ( $c ) = @_;
953 23         115 my $schema = $c->schema;
954 22         91 my $id = $c->item_id;
955 22 100       303 if ( !$id ) {
956 1         5 my $id_field = $schema->id_field;
957 1 50       30 die sprintf "ID field(s) %s not defined in stash",
958             join ', ', map qq("$_"), $id_field eq 'ARRAY' ? @$id_field : $id_field;
959             }
960              
961             # Display the form, if requested. This makes it easy to display
962             # a confirmation page in a single route.
963 21 100       96 if ( $c->req->method eq 'GET' ) {
964 4         91 my $item = $c->clean_item( $schema->get( $id ) );
965 4         64 $c->respond_to(
966             json => {
967             status => 400,
968             json => {
969             errors => [
970             {
971             message => 'GET request for JSON invalid',
972             },
973             ],
974             },
975             },
976             any => { item => $item },
977             );
978 4         23578 return;
979             }
980              
981 17 100 100     366 if ( $c->accepts( 'html' ) && $c->validation->csrf_protect->has_error( 'csrf_token' ) ) {
982 2         2945 $c->app->log->error( 'CSRF token validation failed' );
983 2         46 $c->render(
984             status => 400,
985             item => $c->clean_item( $schema->get( $id ) ),
986             errors => [
987             {
988             message => 'CSRF token invalid.',
989             },
990             ],
991             );
992 2         6090 return;
993             }
994              
995 15         15797 my $item = $schema->get( $id );
996 15   100     308 for my $helper ( @{ $c->stash( 'before_delete' ) // [] } ) {
  15         68  
997 3         56 $c->$helper( $item );
998             }
999             # ID fields could change during helper
1000 15         263 $id = $c->item_id;
1001 15         202 $schema->delete( $id );
1002              
1003             return $c->respond_to(
1004             json => sub {
1005 9     9   3996 $c->rendered( 204 );
1006 9         1608 return;
1007             },
1008             any => sub {
1009 6 100   6   3054 if ( my $route = $c->stash( 'forward_to' ) ) {
1010 4         74 $c->redirect_to( $route );
1011 4         6024 return;
1012             }
1013             },
1014 15         215 );
1015             }
1016              
1017             #pod =method feed
1018             #pod
1019             #pod $routes->websocket( '/' )->to(
1020             #pod 'yancy#feed',
1021             #pod schema => $schema_name,
1022             #pod );
1023             #pod
1024             #pod Subscribe to a feed of changes to the given schema. This first sends a list result
1025             #pod (like L would). Then it sends change messages. Change messages are JSON objects
1026             #pod with different fields based on the method of change:
1027             #pod
1028             #pod # An item in the list was changed
1029             #pod {
1030             #pod method => "set",
1031             #pod # The position of the changed item in the list, 0-based
1032             #pod index => 2,
1033             #pod item => {
1034             #pod # These are the fields that changed
1035             #pod name => 'Lars Fillmore',
1036             #pod },
1037             #pod }
1038             #pod
1039             #pod # An item was added to the list
1040             #pod {
1041             #pod method => "create",
1042             #pod # The position of the new item in the list, 0-based
1043             #pod index => 0,
1044             #pod item => {
1045             #pod # The entire, newly-created item
1046             #pod # ...
1047             #pod },
1048             #pod }
1049             #pod
1050             #pod # An item was removed from the list. This does not necessarily mean
1051             #pod # the item was removed from the database.
1052             #pod {
1053             #pod method => "delete",
1054             #pod # The position of the item removed from the list, 0-based
1055             #pod index => 0,
1056             #pod }
1057             #pod
1058             #pod B Allow the client to send change messages to the server.
1059             #pod
1060             #pod =head4 Input Stash
1061             #pod
1062             #pod This method uses the following stash values for configuration:
1063             #pod
1064             #pod =over
1065             #pod
1066             #pod =item schema
1067             #pod
1068             #pod The schema to use. Required.
1069             #pod
1070             #pod =item limit
1071             #pod
1072             #pod The number of items to show on the page. Defaults to C<10>.
1073             #pod
1074             #pod =item page
1075             #pod
1076             #pod The page number to show. Defaults to C<1>. The page number will
1077             #pod be used to calculate the C parameter to L.
1078             #pod
1079             #pod =item filter
1080             #pod
1081             #pod A hash reference of field/value pairs to filter the contents of the list
1082             #pod or a subref that generates this hash reference. The subref will be passed
1083             #pod the current controller object (C<$c>).
1084             #pod
1085             #pod This overrides any query filters and so can be used to enforce
1086             #pod authorization / security.
1087             #pod
1088             #pod =item order_by
1089             #pod
1090             #pod Set the default order for the items. Supports any L
1091             #pod C structure.
1092             #pod
1093             #pod =item before_render
1094             #pod
1095             #pod An array reference of hooks to call once for each item in the C list
1096             #pod before they are sent as messages. See L for usage.
1097             #pod
1098             #pod =back
1099             #pod
1100             #pod =head4 Query Params
1101             #pod
1102             #pod The following URL query parameters are allowed for this method:
1103             #pod
1104             #pod =over
1105             #pod
1106             #pod =item $page
1107             #pod
1108             #pod Instead of using the C stash value, you can use the C<$page> query
1109             #pod parameter to set the page.
1110             #pod
1111             #pod =item $offset
1112             #pod
1113             #pod Instead of using the C stash value, you can use the C<$offset>
1114             #pod query parameter to set the page offset. This is overridden by the
1115             #pod C<$page> query parameter.
1116             #pod
1117             #pod =item $limit
1118             #pod
1119             #pod Instead of using the C stash value, you can use the C<$limit>
1120             #pod query parameter to allow users to specify their own page size.
1121             #pod
1122             #pod =item $order_by
1123             #pod
1124             #pod One or more fields to order by. Can be specified as C<< >> or
1125             #pod C<< asc: >> to sort in ascending order or C<< desc: >>
1126             #pod to sort in descending order.
1127             #pod
1128             #pod =item $match
1129             #pod
1130             #pod How to match multiple field filters. Can be C or C (default
1131             #pod C). C means all fields must match for a row to be returned.
1132             #pod C means at least one field must match for a row to be returned.
1133             #pod
1134             #pod =item Additional Field Filters
1135             #pod
1136             #pod Any named query parameter that matches a field in the schema will be
1137             #pod used to further filter the results. The stash C will override
1138             #pod this filter, so that the stash C can be used for security.
1139             #pod
1140             #pod =back
1141             #pod
1142             #pod =cut
1143              
1144             sub feed {
1145 1     1 1 21850 my ( $c ) = @_;
1146 1         10 $c->inactivity_timeout( 3600 );
1147 1         158 my $schema = $c->schema;
1148              
1149             # First, send the message for the initial page
1150 1         7 my ( $filter, $opt ) = $c->_get_list_args;
1151 1         8 my $result = $schema->list( $filter, $opt );
1152 1   50     18 for my $helper ( @{ $c->stash( 'before_render' ) // [] } ) {
  1         5  
1153 0         0 $c->$helper( $_ ) for @{ $result->{items} };
  0         0  
1154             }
1155 1         21 my $x_id_field = $schema->id_field;
1156 1 50       15 my @id_fields = ref $x_id_field eq 'ARRAY' ? @$x_id_field : ( $x_id_field );
1157             #; $c->log->debug( 'Original result: ' . $c->dumper( $result ) );
1158 1         13 $c->send({ json => { %$result, method => 'list' } });
1159              
1160             # Now, poll the database for updates every few seconds.
1161             # XXX: Create Yancy::Plugin::PubSub to do push messaging instead of
1162             # ugly polling...
1163             my $id = Mojo::IOLoop->recurring( $c->stash( 'interval' ) // 10, sub {
1164 3     3   2956966 my $new_result = $schema->list( $filter, $opt );
1165             #; $c->log->debug( 'New result: ' . $c->dumper( $new_result ) );
1166 3         57 my %seen_items;
1167             my @created_items;
1168 3         9 NEW_ITEM: for my $new_i ( 0..$#{ $new_result->{items} } ) {
  3         14  
1169 17         31 my $new_item = $new_result->{items}[$new_i];
1170             # Loop through the old result to find the existing items by
1171             # their ID fields
1172 17         29 for my $old_i ( 0..$#{ $result->{items} } ) {
  17         57  
1173 56         99 my $old_item = $result->{items}[$old_i];
1174 56 100       87 if ( @id_fields == grep { $new_item->{ $_ } eq $old_item->{ $_ } } @id_fields ) {
  56         125  
1175             # Found it!
1176 16         55 $seen_items{ $old_i }++;
1177             my %diff =
1178 2         6 map { $_ => $new_item->{ $_ } }
1179 7     7   25435 grep {; no warnings 'uninitialized'; $new_item->{ $_ } ne $old_item->{ $_ } }
  7         20  
  7         8089  
  16         43  
  288         532  
1180             keys %$new_item, keys %$old_item
1181             ;
1182 16 100       63 if ( keys %diff ) {
1183 1         8 my $message = {
1184             method => 'set',
1185             index => $old_i,
1186             item => \%diff,
1187             };
1188             #$c->log->debug( $c->dumper( $message ) );
1189 1         11 $c->send({ json => $message });
1190             }
1191 16         595 next NEW_ITEM;
1192             }
1193             }
1194             # If we can't find the new item, it must have been added.
1195             # Queue it up to send after deletes to maintain indexes.
1196 1         9 push @created_items, {
1197             method => 'create',
1198             index => $new_i,
1199             item => $new_item,
1200             };
1201             }
1202             # Any items we did not see must have been removed from the list,
1203             # or pushed out by newly-created items. Send these in reverse to
1204             # maintain indexes.
1205 3         11 for my $old_i ( reverse grep { !$seen_items{ $_ } } 0..$#{ $result->{items} } ) {
  17         42  
  3         12  
1206 1         6 my $message = {
1207             method => 'delete',
1208             index => $old_i,
1209             };
1210             #$c->log->debug( $c->dumper( $message ) );
1211 1         12 $c->send({ json => $message });
1212             }
1213             # Now we can send the created items, from lowest index to
1214             # highest index
1215 3         440 for my $item ( @created_items ) {
1216             #$c->log->debug( $c->dumper( $item ) );
1217 1         12 $c->send({ json => $item });
1218             }
1219              
1220 3         605 $result = $new_result;
1221 1   50     516 } );
1222 1     1   162 $c->on( finish => sub { Mojo::IOLoop->remove( $id ) } );
  1         5171  
1223             # XXX: Allow client to send "list" message to change the parameters
1224             # of the list. Respond with an entirely new result (not a diff).
1225             # XXX: Allow client to send "create", "set", and "delete" messages
1226             # to create, set, and delete items
1227             }
1228              
1229             sub _get_list_args {
1230 54     54   129 my ( $c ) = @_;
1231              
1232 54   66     253 my $limit = $c->param( '$limit' ) // $c->stash->{ limit } // 10;
      50        
1233             my $offset = $c->param( '$page' ) ? ( $c->param( '$page' ) - 1 ) * $limit
1234             : $c->param( '$offset' ) ? $c->param( '$offset' )
1235 54 100 100     17816 : ( ( $c->stash->{page} // 1 ) - 1 ) * $limit;
    50          
1236 54         6610 $c->stash( page => int( $offset / $limit ) + 1 );
1237 54         1074 my $opt = {
1238             limit => $limit,
1239             offset => $offset,
1240             };
1241              
1242 54 100       177 if ( my $order_by = $c->param( '$order_by' ) ) {
    100          
1243             $opt->{order_by} = [
1244 7 100 66     515 map +{ "-" . ( $_->[1] ? $_->[0] : 'asc' ) => $_->[1] // $_->[0] },
1245             map +[ split /:/ ],
1246             split /,/, $order_by
1247             ];
1248             }
1249             elsif ( $order_by = $c->stash( 'order_by' ) ) {
1250 8         531 $opt->{order_by} = $order_by;
1251             }
1252              
1253 54         2641 my $schema = $c->schema;
1254 52         212 my $props = $schema->json_schema->{properties};
1255 52         304 my %param_filter = ();
1256 52         96 for my $key ( @{ $c->req->params->names } ) {
  52         147  
1257 38 100       1663 next unless exists $props->{ $key };
1258 23   50     104 my $type = $props->{$key}{type} || 'string';
1259 23         80 my $value = $c->param( $key );
1260 23 100       1432 if ( is_type( $type, 'string' ) ) {
    100          
    50          
    0          
1261 17 100       81 if ( ( $value =~ tr/*/%/ ) <= 0 ) {
1262 11         38 $value = "\%$value\%";
1263             }
1264 17         92 $param_filter{ $key } = { -like => $value };
1265             }
1266             elsif ( grep is_type( $type, $_ ), qw(number integer) ) {
1267 3         14 $param_filter{ $key } = $value ;
1268             }
1269             elsif ( is_type( $type, 'boolean' ) ) {
1270 3 50 33     21 $param_filter{ ($value && $value ne 'false')? '-bool' : '-not_bool' } = $key;
1271             }
1272             elsif ( is_type($type, 'array') ) {
1273 0         0 $param_filter{ $key } = { '-has' => $value };
1274             }
1275             else {
1276 0         0 die "Sorry type '" .
1277             to_json( $type ) .
1278             "' is not handled yet, only string|number|integer|boolean|array is supported."
1279             }
1280             }
1281             my $filter = {
1282             %param_filter,
1283             # Stash filter always overrides param filter, for security
1284 52         806 %{ $c->_resolve_filter },
  52         177  
1285             };
1286 52 100 66     226 if ( $c->param( '$match' ) && $c->param( '$match' ) eq 'any' ) {
1287             $filter = [
1288 1         146 map +{ $_ => $filter->{ $_ } }, keys %$filter
1289             ];
1290             }
1291              
1292             #; use Data::Dumper;
1293             #; $c->app->log->info( Dumper $filter );
1294             #; $c->app->log->info( Dumper $opt );
1295              
1296 52         2947 return ( $filter, $opt );
1297             }
1298              
1299             sub _resolve_filter {
1300 55     55   147 my ( $c ) = @_;
1301 55         168 my $filter = $c->stash( 'filter' );
1302 55 100       642 if ( ref $filter eq 'CODE' ) {
1303 2         7 return $filter->( $c );
1304             }
1305 53   100     316 return $filter // {};
1306             }
1307              
1308             1;
1309              
1310             __END__