Catalyst Model #2: Moon phase data

Again, this example is light. It’s bigger, with more features, than our last model—Random quotes—but it’s still not doing anything but abstracting the calls and wrapping the usage of another module; in this case, Astro::MoonPhase with date magick help from Date::Manip.

This article requires that you’ve set-up a test app (MyApp) per the instructions in 10 Catalyst models in 10 days

The model does no serious exception handling or error feedback. A production version would need to. For example, dates before epoch 0 are not supported and will cause a fatal error.

Install the required modules (if not already present)

cpan Astro::MoonPhase Date::Manip

Create the new model

./script/myapp_create.pl model MoonPhase

exists "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/Model" exists "/Users/jinx/depot/sites/MyApp/script/../t" created "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/Model/MoonPhase.pm" created "/Users/jinx/depot/sites/MyApp/script/../t/model_MoonPhase.t"

Fill the model up with goodies

Notice we are using Date::Manip to parse date arguments so we can use a variety of natural language, such as “tomorrow at 10am,” and have it correctly interpreted, converted to epoch seconds, and given to Astro::MoonPhase::phase() . I like Date::Manip and as far as I know it’s still the best at this trick but it’s not something I’d put in a production application. If you want date handling look at DateTime. When not doing human date string parsing, it has the most comprehensive, correct date handling out there.

emacs lib/MyApp/Model/MoonPhase.pm

package MyApp::Model::MoonPhase ; use strict ; use warnings ; use parent 'Catalyst::Model' ; use Astro::MoonPhase (); use Date::Manip qw( ParseDate ParseDateString UnixDate ) ; use Carp ; sub phase { my ( $self , $raw_timish ) = @_ ; Astro::MoonPhase::phase ( _helper_time ( $raw_timish ) ); } sub illumination { [ + shift -> phase ( @_ ) ] -> [ 1 ] } sub age { [ + shift -> phase ( @_ ) ] -> [ 2 ] } sub is_waxing { + shift -> age ( @_ ) < ( 29.53 / 2 ); } sub is_waning { ! + shift -> is_waxing ( @_ ); } sub is_gibbous { + shift -> illlumination ( @_ ) > .5 ; } sub _helper_time { my $raw_date = shift || time (); my $parsed = $raw_date =~ /^(?!19|20)\d{9,10}$/ ? ParseDate ( ParseDateString ( "epoch $raw_date" ) ) : ParseDate ( $raw_date ); my $time = eval { UnixDate ( $parsed , "%s" ) }; croak "Sorry, bad date: $@; got $parsed parsing $raw_date" if $@ ; return $time ; } 1 ;

Make a controller to consume the model’s data

/script/myapp_create.pl controller MoonPhase

exists "/Users/jinx/MyApp/script/../lib/MyApp/Controller" exists "/Users/jinx/MyApp/script/../t" created "/Users/jinx/MyApp/script/../lib/MyApp/Controller/MoonPhase.pm" created "/Users/jinx/MyApp/script/../t/controller_MoonPhase.t"

emacs lib/MyApp/Controller/MoonPhase.pm

package MyApp::Controller::MoonPhase ; use strict ; use warnings ; use parent 'Catalyst::Controller' ; sub index : Path Args ( 0 ) { my ( $self , $c ) = @_ ; $c -> detach ( "with_arg" , [ "now" ]); } sub with_arg : Path Args ( 1 ) { my ( $self , $c , $when ) = @_ ; $c -> response -> content_type ( "text/plain; charset=utf-8" ); my $phase = sprintf ( "The moon $when: %.1f%% full and %s." , $c -> model ( "MoonPhase" ) -> illumination ( $when ) * 100 , $c -> model ( "MoonPhase" ) -> is_waxing ( $when ) ? "waxing" : "waning" , ); $c -> response -> body ( $phase ); } 1 ;

Note that the with_arg method accepts one argument in its path. This is the time string, natural language or otherwise, to lookup. If called without an argument, you get index instead which sets the argument to “now” and detaches to with_arg . The model has a similar safety/default. Notice that MyApp::Model->_helper_time in the model fills in the argument if not provided.

my $raw_date = shift || time();

Also of note is the content type we set for the output.

$c->response->content_type("text/plain; charset=utf-8");

We are accepting user input in the controller. It is crucial that you never, ever, ever, ever, ever make a song about the Sibbie. It’s also somewhat important that you do not ever echo user input back to the browser. If you do, and your content is HTML or any medium which supports executables, you have just made your site insecure.

We set the output as plain text. This means JavaScript is neutered and XSS attacks are less likely. You can test it yourself with a “time” argument like-

%3Cscript%3Ealert(%22I%20can%20haz%20cookies%22)%3C%2Fscript%3E

To see the difference try swapping out the content type line with-

$c->response->content_type("text/html; charset=utf-8");

–and see how it goes. It’s worth noting that Internet Explorer sometimes ignores the content type header in favor of its own content based heuristics.

Now hit your test server and try it out…

./script/myapp_server.pl -d -r -p 3000

Tomorrow we try Catalyst Model #3: Cover images via Amazon.com’s APA .