File Coverage

blib/lib/Lavoco/Website.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Lavoco::Website;
2              
3 1     1   19297 use 5.006;
  1         4  
  1         39  
4              
5 1     1   259 use Moose;
  0            
  0            
6              
7             use Data::Dumper;
8             use DateTime;
9             use Email::Stuffer;
10             use Encode;
11             use File::Slurp;
12             use FindBin qw($Bin);
13             use JSON;
14             use Log::AutoDump;
15             use Plack::Handler::FCGI;
16             use Plack::Request;
17             use Template;
18             use Term::ANSIColor;
19             use Time::HiRes qw(gettimeofday);
20              
21             $Data::Dumper::Sortkeys = 1;
22              
23             =head1 NAME
24              
25             Lavoco::Website - EXPERIMENTAL FRAMEWORK.
26              
27             Framework to run small websites, URL dispatching based on a flexible config file, to render Template::Toolkit templates.
28              
29             =head1 VERSION
30              
31             Version 0.12
32              
33             =cut
34              
35             our $VERSION = '0.12';
36              
37             $VERSION = eval $VERSION;
38              
39             =head1 SYNOPSIS
40              
41             A FastCGI web-app for serving Template::Toolkit templates.
42              
43             This is an experimental framework to control various small websites, use at your own risk.
44              
45             use Lavoco::Website;
46            
47             my $website = Lavoco::Website->new( name => 'Example' );
48            
49             my $action = lc( $ARGV[0] ); # (start|stop|restart)
50            
51             $website->$action;
52              
53             =cut
54              
55             =head1 METHODS
56              
57             =head2 Class Methods
58              
59             =head3 new
60              
61             Creates a new instance of the website object.
62              
63             =head2 Instance Methods
64              
65             =cut
66              
67             has name => ( is => 'rw', isa => 'Str', default => 'Website' );
68             has processes => ( is => 'rw', isa => 'Int', default => 5 );
69             has base => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build_base' );
70             has dev => ( is => 'rw', isa => 'Bool', lazy => 1, builder => '_build_dev' );
71             has _pid => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build__pid' );
72             has _socket => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build__socket' );
73             has templates => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build_templates' );
74              
75             sub _build_base
76             {
77             return $Bin;
78             }
79              
80             sub _build_dev
81             {
82             my $self = shift;
83              
84             return 0 if $self->base =~ m:/live:;
85              
86             return 1;
87             }
88              
89             sub _build__pid
90             {
91             my $self = shift;
92              
93             return $self->base . '/website.pid';
94             }
95              
96             sub _build__socket
97             {
98             my $self = shift;
99              
100             return $self->base . '/website.sock';
101             }
102              
103             sub _build_templates
104             {
105             my $self = shift;
106              
107             return $self->base . '/templates';
108             }
109              
110             =head3 name
111              
112             The identifier for the website, used as the process title.
113              
114             =head3 base
115              
116             The base directory of the application, using L<FindBin>.
117              
118             =head3 dev
119              
120             A simple boolean flag to indicate whether you're running a development instance of the website.
121              
122             It's on by default, and currently turned off if the base directory contains C</live>. Feel free to set it based on your own logic before calling C<start()>.
123              
124             I typically use working directories such as C</home/user/www.example.com/dev> and C</home/user/www.example.com/live>.
125              
126             This flag is useful to disable things like Google Analytics on the dev site.
127              
128             The website object is avilable to all templates under the name C<website>.
129              
130             e.g. C<[% IF website.dev %]...>
131              
132             =head3 processes
133              
134             Number of FastCGI process to spawn, 5 by default.
135              
136             $website->processes( 10 );
137              
138             =head3 templates
139              
140             The directory containing the TT templates, by default it's C<$website-E<gt>base . '/templates'>.
141              
142             =head3 start
143              
144             Starts the FastCGI daemon. Performs basic checks of your environment and dies if there's a problem.
145              
146             =cut
147              
148             sub start
149             {
150             my $self = shift;
151              
152             if ( -e $self->_pid )
153             {
154             print "PID file " . $self->_pid . " already exists, I think you should kill that first, or specify a new pid file with the -p option\n";
155            
156             return $self;
157             }
158              
159             $self->_init;
160              
161             print "Building FastCGI engine...\n";
162            
163             my $server = Plack::Handler::FCGI->new(
164             nproc => $self->processes,
165             listen => [ $self->_socket ],
166             pid => $self->_pid,
167             detach => 1,
168             proc_title => $self->name,
169             );
170            
171             $server->run( $self->_handler );
172             }
173              
174             sub _init
175             {
176             my ( $self, %args ) = @_;
177              
178             ###############################
179             # make sure there's a log dir #
180             ###############################
181              
182             printf( "%-50s", "Checking logs directory");
183              
184             my $log_dir = $self->base . '/logs';
185              
186             if ( ! -e $log_dir || ! -d $log_dir )
187             {
188             _print_red( "[ FAIL ]\n" );
189             print $log_dir . " does not exist, or it's not a folder.\nExiting...\n";
190             exit;
191             }
192              
193             _print_green( "[ OK ]\n" );
194              
195             #####################################
196             # make sure there's a templates dir #
197             #####################################
198              
199             printf( "%-50s", "Checking templates directory");
200              
201             if ( ! -e $self->templates || ! -d $self->templates )
202             {
203             _print_red( "[ FAIL ]\n" );
204             print $self->templates . " does not exist, or it's not a folder.\nExiting...\n";
205             exit;
206             }
207              
208             _print_green( "[ OK ]\n" );
209              
210             ###########################
211             # make sure 404.tt exists #
212             ###########################
213              
214             printf( "%-50s", "Checking 404 template");
215              
216             my $template_404_file = $self->templates . '/404.tt';
217              
218             if ( ! -e $template_404_file )
219             {
220             _print_red( "[ FAIL ]\n" );
221             print $template_404_file . " does not exist.\nExiting...\n";
222             exit;
223             }
224              
225             _print_green( "[ OK ]\n" );
226              
227             ########################
228             # load the config file #
229             ########################
230              
231             printf( "%-50s", "Checking config");
232              
233             my $config_file = $self->base . '/website.json';
234              
235             if ( ! -e $config_file )
236             {
237             _print_red( "[ FAIL ]\n" );
238             print $config_file . " does not exist.\nExiting...\n";
239             exit;
240             }
241              
242             my $string = read_file( $config_file, { binmode => ':utf8' } );
243              
244             my $config = undef;
245              
246             eval {
247             $config = decode_json $string;
248             };
249              
250             if ( $@ )
251             {
252             _print_red( "[ FAIL ]\n" );
253             print "Config file error...\n" . $@ . "Exiting...\n";
254             exit;
255             }
256              
257             ###################################
258             # basic checks on the config file #
259             ###################################
260              
261             if ( ! $config->{ pages } )
262             {
263             _print_red( "[ FAIL ]\n" );
264             print "'pages' attribute missing at top level.\nExiting...\n";
265             exit;
266             }
267              
268             if ( ref $config->{ pages } ne 'ARRAY' )
269             {
270             _print_red( "[ FAIL ]\n" );
271             print "'pages' attribute is not a list.\nExiting...\n";
272             exit;
273             }
274              
275             if ( scalar @{ $config->{ pages } } == 0 )
276             {
277             _print_organge( "[ISSUE]\n" );
278             print "No 'pages' defined in config, this will result in a 404 for all requests.\n";
279             }
280              
281             my %paths = ();
282              
283             foreach my $each_page ( @{ $config->{ pages } } )
284             {
285             if ( ! $each_page->{ path } )
286             {
287             _print_red( "[ FAIL ]\n" );
288             print "'path' attribute missing for page..." . ( Dumper $each_page );
289             exit;
290             }
291              
292             if ( ! $each_page->{ template } )
293             {
294             _print_red( "[ FAIL ]\n" );
295             print "'template' attribute missing for page..." . ( Dumper $each_page );
296             exit;
297             }
298              
299             if ( exists $paths{ $each_page->{ path } } )
300             {
301             _print_red( "[ FAIL ]\n" );
302             print "Path '" . $each_page->{ path } . "' found more than once.\nExiting...\n";
303             exit;
304             }
305              
306             $paths{ $each_page->{ path } } = 1;
307             }
308              
309             _print_green( "[ OK ]\n" );
310              
311             return $self;
312             }
313              
314             sub _print_green
315             {
316             my $string = shift;
317             print color 'bold green';
318             print $string;
319             print color 'reset';
320             }
321              
322             sub _print_orange
323             {
324             my $string = shift;
325             print color 'bold orange';
326             print $string;
327             print color 'reset';
328             }
329              
330             sub _print_red
331             {
332             my $string = shift;
333             print color 'bold red';
334             print $string;
335             print color 'reset';
336             }
337              
338             =head3 stop
339              
340             Stops the FastCGI daemon.
341              
342             =cut
343              
344             sub stop
345             {
346             my $self = shift;
347              
348             if ( ! -e $self->_pid )
349             {
350             return $self;
351             }
352            
353             open( my $fh, "<", $self->_pid ) or die "Cannot open pidfile: $!";
354              
355             my @pids = <$fh>;
356              
357             close $fh;
358              
359             chomp( $pids[0] );
360              
361             print "Killing pid $pids[0] ...\n";
362              
363             kill 15, $pids[0];
364              
365             return $self;
366             }
367              
368             =head3 restart
369              
370             Restarts the FastCGI daemon, with a 1 second delay between stopping and starting.
371              
372             =cut
373              
374             sub restart
375             {
376             my $self = shift;
377            
378             $self->stop;
379              
380             sleep 1;
381              
382             $self->start;
383              
384             return $self;
385             }
386              
387             =head1 CONFIG
388              
389             Currently only JSON is supported, the config file should be named C<website.json> and should be placed in the base directory of your website.
390              
391             See the C<examples> directory for a sample JSON config file.
392              
393             {
394             "pages" : [
395             {
396             "path" : "/",
397             "template":"index.tt",
398             ...
399             },
400             ...
401             ]
402             ...
403             "send_alerts_from":"The Example Website <no-reply@example.com>",
404             "send_404_alerts_to":"you@example.com",
405             ...
406             }
407              
408             The entire config hash is available in all templates via [% config %], and is flexible, there are only a couple of mandatory/reserved attributes.
409              
410             The mandatory field in the config is C<pages>, which is an array of JSON objects.
411              
412             Each C<page> object should have a C<path> (to match a URL) and C<template>.
413              
414             All other fields are up to you, to fit your requirements.
415              
416             When a request is made, a lookup is performed for a page by matching the C<path> - the associated C<template> will be rendered.
417              
418             If no page is found, the template C<404.tt> will be rendered, make sure you have this file ready in the templates directory.
419              
420             The C<page> object is available in the templates.
421              
422             It is often useful to have sub-pages, simply create a C<pages> attribute in a C<page> object.
423              
424             If a sub-page is selected, an extra key for C<parents> is included in the C<page> object as a list of the parent pages, this is useful for building breadcrumb links.
425              
426             =cut
427              
428             # returns a code-ref for the FCGI handler/server.
429              
430             sub _handler
431             {
432             my $self = shift;
433              
434             return sub {
435              
436             ##############
437             # initialise #
438             ##############
439              
440             my $req = Plack::Request->new( shift );
441              
442             my %stash = (
443             website => $self,
444             req => $req,
445             now => DateTime->now,
446             started => join( '.', gettimeofday ),
447             );
448              
449             my $log = Log::AutoDump->new( base_dir => $stash{ website }->base . '/logs', filename => 'website.log' );
450              
451             $log->debug("Started");
452              
453             my $path = $req->uri->path;
454              
455             $log->debug( "Requested path: " . $path );
456              
457             ##################
458             # get the config #
459             ##################
460              
461             $stash{ config } = $stash{ website }->_get_config( log => $log );
462              
463             ###############
464             # sitemap xml #
465             ###############
466              
467             if ( $path eq '/sitemap.xml' )
468             {
469             return $stash{ website }->_sitemap( log => $log, req => $req, stash => \%stash );
470             }
471              
472             ##########################################################################
473             # find a matching 'page' from the config that matches the requested path #
474             ##########################################################################
475              
476             foreach my $each_page ( @{ $stash{ config }->{ pages } } )
477             {
478             if ( $path eq $each_page->{ path } )
479             {
480             $stash{ page } = $each_page;
481              
482             last;
483             }
484              
485             if ( ref $each_page->{ pages } eq 'ARRAY' )
486             {
487             foreach my $each_sub_page ( @{ $each_page->{ pages } } )
488             {
489             if ( $path eq $each_sub_page->{ path } )
490             {
491             $stash{ page } = $each_sub_page;
492              
493             $stash{ page }->{ parents } = [];
494            
495             push @{ $stash{ page }->{ parents } }, $each_page;
496            
497             last;
498             }
499             }
500             }
501             }
502              
503             $log->debug( "Matching page found in config" ) if exists $stash{ page };
504              
505             #######
506             # 404 #
507             #######
508            
509             if ( ! exists $stash{ page } )
510             {
511             return $stash{ website }->_404( log => $log, req => $req, stash => \%stash );
512             }
513              
514             my $res = $req->new_response;
515              
516             $res->status( 200 );
517              
518             my $tt = Template->new( ENCODING => 'UTF-8', INCLUDE_PATH => $stash{ website }->templates );
519              
520             $log->debug("Processing template: " . $stash{ website }->templates . "/" . $stash{ page }->{ template } );
521              
522             my $body = '';
523              
524             $tt->process( $stash{ page }->{ template }, \%stash, \$body ) or $log->debug( $tt->error );
525              
526             $res->content_type('text/html; charset=utf-8');
527              
528             $res->body( encode( "UTF-8", $body ) );
529              
530             $stash{ took } = join( '.', gettimeofday ) - $stash{ started };
531            
532             $log->debug( "The stash contains...", \%stash );
533            
534             $log->debug( "Took " . sprintf("%.5f", $stash{ took } ) . " seconds");
535              
536             #######################################
537             # cleanup (circular references, etc.) #
538             #######################################
539              
540             delete $stash{ page }->{ parents } if exists $stash{ page };
541              
542             return $res->finalize;
543             }
544             }
545              
546             sub _sitemap
547             {
548             my ( $self, %args ) = @_;
549              
550             my $log = $args{ log };
551             my $req = $args{ req };
552             my $stash = $args{ stash };
553              
554             my $base = ($req->env->{'psgi.url_scheme'} || "http") .
555             "://" . ($req->env->{HTTP_HOST} || (($req->env->{SERVER_NAME} || "") . ":" . ($req->env->{SERVER_PORT} || 80)));
556              
557             my $sitemap = '<?xml version="1.0" encoding="UTF-8"?>';
558              
559             $sitemap .= "\n";
560              
561             $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">';
562              
563             $sitemap .= "\n";
564              
565             foreach my $each_page ( @{ $stash->{ config }->{ pages } } )
566             {
567             $sitemap .= "<url><loc>" . $base . $each_page->{ path } . "</loc></url>\n";
568              
569             if ( ref $each_page->{ pages } eq 'ARRAY' )
570             {
571             foreach my $each_sub_page ( @{ $each_page->{ pages } } )
572             {
573             $sitemap .= "<url><loc>" . $base . $each_sub_page->{ path } . "</loc></url>\n";
574             }
575             }
576             }
577            
578             $sitemap .= "</urlset>\n";
579              
580             my $res = $req->new_response;
581              
582             $res->status(200);
583              
584             $res->content_type('application/xml; charset=utf-8');
585            
586             $res->body( encode( "UTF-8", $sitemap ) );
587              
588             return $res->finalize;
589             }
590              
591             sub _404
592             {
593             my ( $self, %args ) = @_;
594              
595             my $log = $args{ log };
596             my $req = $args{ req };
597             my $stash = $args{ stash };
598              
599             $stash->{ page } = { template => '404.tt' };
600              
601             if ( $stash->{ config }->{ send_alerts_from } && $stash->{ config }->{ send_404_alerts_to } )
602             {
603             $stash->{ website }->_send_email(
604             from => $stash->{ config }->{ send_alerts_from },
605             to => $stash->{ config }->{ send_404_alerts_to },
606             subject => "404 - " . $req->uri,
607             text_body => "404 - " . $req->uri . "\n\nReferrer: " . ( $req->referer || 'None' ) . "\n\n" . Dumper( $req ) . "\n\n" . Dumper( \%ENV ),
608             );
609             }
610              
611             my $res = $req->new_response;
612              
613             $res->status( 404 );
614              
615             $res->content_type('text/html; charset=utf-8');
616              
617             my $tt = Template->new( ENCODING => 'UTF-8', INCLUDE_PATH => $stash->{ website }->templates );
618              
619             $log->debug("Processing template: " . $stash->{ website }->templates . "/" . $stash->{ page }->{ template } );
620              
621             my $body = '';
622              
623             $tt->process( $stash->{ page }->{ template }, $stash, \$body ) or $log->debug( $tt->error );
624              
625             $res->content_type('text/html; charset=utf-8');
626              
627             $res->body( encode( "UTF-8", $body ) );
628              
629             return $res->finalize;
630             }
631              
632             sub _get_config
633             {
634             my ( $self, %args ) = @_;
635              
636             my $log = $args{ log };
637              
638             my $filename = $self->base . '/website.json';
639              
640             $log->debug( "Opening config file: " . $filename );
641              
642             my $string = read_file( $filename, { binmode => ':utf8' } );
643              
644             my $config = undef;
645              
646             eval {
647             $config = decode_json $string;
648             };
649              
650             $log->debug( $@ ) if $@;
651              
652             return $config;
653             }
654              
655             sub _send_email
656             {
657             my ( $self, %args ) = @_;
658              
659             if ( $args{ to } )
660             {
661             Email::Stuffer->from( $args{ from } )
662             ->to( $args{ to } )
663             ->subject( $args{ subject } )
664             ->text_body( $args{ text_body } )
665             ->send;
666             }
667              
668             return $self;
669             }
670              
671             =head1 TODO
672              
673             More documentation.
674              
675             =head1 AUTHOR
676              
677             Rob Brown, C<< <rob at intelcompute.com> >>
678              
679             =head1 BUGS
680              
681             Please report any bugs or feature requests to C<bug-lavoco-website at rt.cpan.org>, or through
682             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Lavoco-Website>. I will be notified, and then you will
683             automatically be notified of progress on your bug as I make changes.
684              
685             =head1 SUPPORT
686              
687             You can find documentation for this module with the perldoc command.
688              
689             perldoc Lavoco::Website
690              
691              
692             You can also look for information at:
693              
694             =over 4
695              
696             =item * RT: CPAN's request tracker (report bugs here)
697              
698             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Lavoco-Website>
699              
700             =item * AnnoCPAN: Annotated CPAN documentation
701              
702             L<http://annocpan.org/dist/Lavoco-Website>
703              
704             =item * CPAN Ratings
705              
706             L<http://cpanratings.perl.org/d/Lavoco-Website>
707              
708             =item * Search CPAN
709              
710             L<http://search.cpan.org/dist/Lavoco-Website/>
711              
712             =back
713              
714             =head1 LICENSE AND COPYRIGHT
715              
716             Copyright 2014 Rob Brown.
717              
718             This program is free software; you can redistribute it and/or modify it
719             under the terms of either: the GNU General Public License as published
720             by the Free Software Foundation; or the Artistic License.
721              
722             See http://dev.perl.org/licenses/ for more information.
723              
724             =cut
725              
726             1;
727