File Coverage

blib/lib/Yancy/Controller/Yancy.pm
Criterion Covered Total %
statement 257 262 98.0
branch 85 94 90.4
condition 70 93 75.2
subroutine 25 25 100.0
pod 8 8 100.0
total 445 482 92.3


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