root/veekun/trunk/lib/Vee/Controller/Root.pm

Revision 474, 8.3 KB (checked in by eevee, 12 months ago)

Fixed a handful of warnings.

Line 
1package Vee::Controller::Root;
2
3use strict;
4use warnings;
5use base 'Catalyst::Controller';
6
7use Vee::Authorization;
8use Vee::Utils;
9
10use Data::Dumper;
11use DBIx::Class::QueryLog;
12use File::Path;
13use Time::HiRes;
14use Time::Out qw/timeout/;
15use URI;
16use XML::Feed;
17
18# Sets the actions in this controller to be registered with no prefix
19# so they function identically to actions created in Vee.pm
20__PACKAGE__->config->{namespace} = '';
21
22=head1 NAME
23
24Vee::Controller::Root - Root Controller
25
26=head1 SYNOPSIS
27
28See L<Vee>.
29
30=head1 DESCRIPTION
31
32Root Controller, containing global defaults and begin/end actions.
33
34=head1 METHODS
35
36=cut
37
38=head2 auto
39
40=cut
41
42sub auto : Private {
43    my ($self, $c) = @_;
44    my $s = $c->stash;
45   
46    # for execution time
47    $s->{start_time} = Time::HiRes::time;
48   
49    # track SQL queries
50    my $storage = $c->model('DBIC')->schema->storage;
51    $s->{querylog} = new DBIx::Class::QueryLog;
52    $storage->debugobj($s->{querylog});
53    $storage->debug(1);
54
55    if ($c->user) { $c->user->obj->time_active(time); $c->user->obj->update; }
56   
57    if ($c->flash and %{$c->flash}) {
58        $s->{$_} = $c->flash->{$_} for qw/error_msg info_msg success_msg/;
59    }
60
61    $self->_do_periodic_tasks($c);
62       
63    return 1;
64}
65
66=head2 default
67
68=cut
69
70sub default : Private {
71    my ($self, $c) = @_;
72
73    my @path_parts = @{ $c->req->args };
74
75    if (not -e $c->path_to('static', @path_parts) . '.tt') {
76        $c->stash->{template} = '404.tt';
77        return;
78    }
79   
80    $c->stash->{additional_template_paths} = [ $c->path_to('static') ];
81    $c->stash->{template} = join('/', @path_parts) . '.tt';
82
83    if ($path_parts[0] eq 'dex') {
84        $c->stash( %Vee::Dex::all );
85    }
86
87    # TODO: 404?
88}
89
90=head2 end
91
92End action.  Does various debugging, error-handling, statistics, and caching
93thingies.
94
95=cut
96
97sub end : ActionClass('RenderView') {
98    my ($self, $c) = @_;
99    my $s = $c->stash;
100
101    # capture flagrant errors in prod
102    if (not $c->debug and scalar @{ $c->error }) {
103        eval {
104            $c->model('DBIC::ErrorLog')->create({
105                time    => time,
106                user_id => $c->user ? $c->user->obj->id : 0,
107                ip      => Vee::Utils::inet_aton($c->req->address),
108                path    => $c->req->path,
109                method  => $c->req->method,
110                query   => Dumper($c->req->params),
111                error   => join "\n", @{$c->error},
112            });
113        };
114
115        $c->res->status(500);
116       
117        $s->{insert_error} = $@;
118        $s->{errors}       = $c->error;  # XXX: not sure if I should expose these to the user
119
120        $s->{template} = 'fatal.tt';
121       
122        $c->forward('Vee::View::TT');
123        $c->error(0);
124    }
125
126    # count up SQL queries used
127    if ($c->debug or $c->req->params->{dump_sql}) {
128        my %perl_queries;
129        for my $qt (@{ $s->{querylog}->log }) {
130            if ($qt->isa('DBIx::Class::QueryLog::Query')) {
131                $perl_queries{ $qt->sql }++
132            } else {
133                $perl_queries{ $_->sql }++ for @{ $qt->queries }
134            }
135        }
136
137        # can't generate this list here as it won't count SQL run in the template
138        $s->{generate_query_list} = sub {
139            my ($log) = @_;
140            # group queries together...
141            my %queries;
142
143            # List of actual query objects; log may contain transactions
144            my @queries;
145            for my $query_or_txn (@$log) {
146                if ($query_or_txn->can('queries')) {
147                    # Transaction
148                    push @queries, @{ $query_or_txn->queries };
149                }
150                else {
151                    push @queries, $query_or_txn;
152                }
153            }
154
155            for my $query (@queries) {
156                $queries{ $query->sql } ||= {
157                    count    => 0,
158                    count_pl => 0,
159                    time     => 0,
160                    sql      => $query->sql,
161                };
162
163                $queries{ $query->sql }{count} += $query->count;
164                $queries{ $query->sql }{time} += $query->time_elapsed;
165            }
166            for my $query (keys %perl_queries) {
167                $queries{$query}{count_pl} = $perl_queries{$query};
168            }
169
170            # ...and sort by time taken
171            my @sorted_queries = sort { $b->{time} <=> $a->{time} } values %queries;
172            return \@sorted_queries;
173        }
174    }
175
176    # if _cache_fh is set, grab the output manually and cache it to disk
177    # NOTE: THIS EXPECTS THAT A WRAPPER IS USED
178    if ($s->{_cache_fh}) {
179        # disable wrapper, then get just the body
180        my $skip_wrapper = $s->{skip_wrapper};
181        $s->{skip_wrapper} = 1;
182        my $body = $c->view->render($c, $s->{template});
183
184        # cache
185        my $cache_file = $s->{_cache_fh};
186        print $cache_file $body;
187        flock $cache_file, 8;
188        close $cache_file;
189
190        $s->{skip_wrapper} = $skip_wrapper;
191        $c->forward('/render_from_cache');
192
193    # this is done here so the above querylog/etc code can run first
194    } elsif ($s->{from_cache}) {
195        $c->forward('/render_from_cache');
196    }
197}
198
199=head2 cache
200
201Uses the template in the stash and a provided scalar as a key to cache this
202page.  If the cache already exists, it will be used and the rest of the calling
203controller will be skipped.
204
205=cut
206
207sub cache : Private {
208    my ($self, $c, $key) = @_;
209    my $s = $c->stash;
210
211    return if $c->debug;
212    return if not $s->{template};
213
214    $key = '' if not defined $key;
215    $key =~ tr/-_a-zA-Z0-9//cd;
216    $key = 'default' if not length $key;
217
218    $s->{_cache_file} = "$s->{template}/$key";
219    my $cache_path = $c->path_to(qw/ tmp cache /) . '/' . $s->{_cache_file};
220
221    if (-e $cache_path) {
222        # if the cache file exists, flag end() to spit it out
223        $s->{from_cache} = 1;
224        $c->detach;
225    } else {
226        # create directory tree; if there be a problem, just don't cache
227        eval { mkpath( $c->path_to(qw/ tmp cache /) . '/' . $s->{template} ) };
228        return if $@;
229
230        # open here and flock to prevent race condition
231        open my $cache_file, '>:encoding(UTF-8)',
232            $c->path_to(qw/ tmp cache /, $s->{_cache_file});
233        flock $cache_file, 2;
234        $s->{_cache_fh} = $cache_file;
235    }
236}
237
238=head2 render_from_cache
239
240Used to...  render a template from the cache.  Mostly used as a hack to avoid
241template compilation, but I'd also like to figure out how to get TT to simply
242not parse the cached file at all.
243
244=cut
245
246sub render_from_cache : Private {
247    my ($self, $c) = @_;
248    my $s = $c->stash;
249
250    open my $cache_file, '<:encoding(UTF-8)',
251        $c->path_to(qw/ tmp cache /, $s->{_cache_file});
252    $c->res->body( $c->view->render($c, $cache_file) );
253    close $cache_file;
254}
255
256=head2 _do_periodic_tasks
257
258Performs various actions if they haven't been done for some amount of time.
259
260Regular method!  Not an action.
261
262=cut
263
264sub _do_periodic_tasks
265{
266    my ($self, $c) = @_;
267
268    if ($c->cache->{_last_performed} and
269        $c->cache->{_last_performed} > time - 15 * 60)
270    {
271        return;
272    }
273
274    # Last subversion commit
275eval {
276    open my $svn_fh, '-|', 'svn', 'log', '--limit' => 1, '--incremental', $c->site_opts->{svn_url};
277    my (undef, $details, undef, @log) = <$svn_fh>;
278    close $svn_fh;
279    die "Couldn't get svn details" if not defined $details;
280
281    my ($rev, $user, $time, undef) = split / \| /, $details;
282    $rev =~ s/^r//;
283    my ($yr, $mo, $day, $hr, $min, $sec, $tz) = split /[- :]/, $time, 7;
284
285    $c->cache->{svn} = {
286        revision     => $rev,
287        user         => $user,
288        time         => new DateTime(
289            year   => $yr,
290            month  => $mo,
291            day    => $day,
292            hour   => $hr,
293            minute => $min,
294            second => $sec,
295            time_zone => substr $tz, 0, 5,
296        ),
297        log          => join '', @log,
298    };
299};
300
301    # Refresh Bulbapedia feeds
302eval {
303    timeout 5, sub {
304        my $uri = URI->new($c->site_opts->{rss_url});
305        my @entries = XML::Feed->parse($uri)->entries;
306        @entries = grep { defined }
307            @entries[ 0 .. $c->site_opts->{page_sizes}{index}{rss} - 1 ];
308        $c->cache->{rss_entries} = \@entries;
309    };
310};
311
312    $c->cache->{_last_performed} = time;
313}
314
315=head1 AUTHOR
316
317Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
318
319See the included F<AUTHORS> file for a full list of contributers.
320
321=head1 LICENSE
322
323See the included F<LICENSE> file.
324
325=cut
326
3271;
Note: See TracBrowser for help on using the browser.