File Coverage

blib/lib/Mojolicious/Plugin/Export.pm
Criterion Covered Total %
statement 94 98 95.9
branch 43 50 86.0
condition 13 18 72.2
subroutine 8 8 100.0
pod 2 2 100.0
total 160 176 90.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Export;
2             our $VERSION = '0.007';
3             # ABSTRACT: Export a Mojolicious website to static files
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod get '/' => 'index';
9             #pod get '/secret' => 'secret';
10             #pod plugin Export => {
11             #pod pages => [qw( / /secret )],
12             #pod };
13             #pod app->start;
14             #pod
15             #pod =head1 DESCRIPTION
16             #pod
17             #pod Export a Mojolicious webapp to static files.
18             #pod
19             #pod =head2 Configuration
20             #pod
21             #pod Default values for the command's options can be specified in the
22             #pod configuration using one of Mojolicious's configuration plugins.
23             #pod
24             #pod # myapp.conf
25             #pod {
26             #pod export => {
27             #pod # Configure the default pages to export
28             #pod pages => [ '/', '/hidden' ],
29             #pod # The directory to export to
30             #pod to => '/var/www/html',
31             #pod # Rewrite URLs to include base directory
32             #pod base => '/',
33             #pod }
34             #pod }
35             #pod
36             #pod =head1 HELPERS
37             #pod
38             #pod =head2 export
39             #pod
40             #pod The C helper returns the L object.
41             #pod
42             #pod =head1 EVENTS
43             #pod
44             #pod =head2 before_write
45             #pod
46             #pod Emitted after all the content has been loaded and prepared for export.
47             #pod The event is given two arguments: The Mojolicious::Plugin::Export object
48             #pod and a hashref of paths mapped to content to be exported. The content can
49             #pod be either a Mojo::DOM object for HTML pages or the content to be
50             #pod exported. This event may modify the hashref or the DOM objects to change
51             #pod the content before it is written.
52             #pod
53             #pod app->export->on( before_write => sub {
54             #pod my ( $export, $pages ) = @_;
55             #pod for my $path ( keys %$pages ) {
56             #pod my $content = $pages->{ $path };
57             #pod # ...
58             #pod }
59             #pod } );
60             #pod
61             #pod =head1 SEE ALSO
62             #pod
63             #pod L, L
64             #pod
65             #pod =cut
66              
67 2     2   23826 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         15  
68 2     2   345 use Mojo::Base 'Mojo::EventEmitter';
  2         5  
  2         13  
69 2     2   281 use Mojo::File qw( path );
  2         5  
  2         120  
70 2     2   13 use Mojo::Util qw( encode decode );
  2         4  
  2         3357  
71              
72             #pod =attr pages
73             #pod
74             #pod The pages to export by default. This can be overridden by the arguments to
75             #pod L.
76             #pod
77             #pod # Add pages to export by default
78             #pod push @{ $app->export->pages }, '/blog';
79             #pod
80             #pod =cut
81              
82             has pages => sub { [] };
83              
84             #pod =attr to
85             #pod
86             #pod The path to export to by default.
87             #pod
88             #pod =cut
89              
90             has to => '.';
91              
92             #pod =attr base
93             #pod
94             #pod The base URL, if URLs need to be rewritten.
95             #pod
96             #pod =cut
97              
98             has base => '';
99              
100             #pod =attr quiet
101             #pod
102             #pod If true, will not report every action taken by the plugin. Defaults to true.
103             #pod
104             #pod =cut
105              
106             has quiet => 1;
107              
108             has _app =>;
109              
110             sub register {
111 8     8 1 526 my ( $self, $app, $plugin_conf ) = @_;
112 8         36 $self->_app( $app );
113 8     9   113 $app->helper( export => sub { $self } );
  9         3959  
114              
115             # Config file overrides plugin config
116 8 50       705 my $config = $app->can( 'config' ) ? $app->config->{export} : {};
117 8         258 for my $key ( keys %$config ) {
118 3 50       23 if ( !$self->can( $key ) ) {
119 0         0 die "Unrecognized export configuration: $key\n";
120             }
121 3   33     15 $self->$key( $config->{ $key } // $plugin_conf->{ $key } );
122             }
123              
124 8         35 return $self;
125             }
126              
127             #pod =method export
128             #pod
129             #pod app->export->export( $override );
130             #pod
131             #pod Export the site. C<$override> is a hash reference to override the object
132             #pod attributes (keys are attribute names, values are the overridden value).
133             #pod
134             #pod =cut
135              
136             sub export {
137 8     8 1 40 my ( $self, $opt ) = @_;
138 8         42 for my $key ( qw( pages to base quiet ) ) {
139 32   100     199 $opt->{ $key } //= $self->$key;
140             }
141              
142 8 50 66     53 if ( $opt->{base} && $opt->{base} =~ m{^[^/]} ) {
143 0         0 $opt->{base} = '/' . $opt->{base};
144             }
145              
146 8         34 my $root = path( $opt->{ to } );
147             my @pages
148 8 50       82 = @{ $opt->{pages} } ? map { m{^/} ? $_ : "/$_" } @{ $opt->{pages} }
  8 100       31  
  5         35  
  5         18  
149             : ( '/' );
150              
151 8         59 my $ua = Mojo::UserAgent->new;
152 8         67 $ua->server->app( $self->_app );
153              
154             # A hash of path => knowledge about the path
155             # link_from => a hash of path -> array of DOM elements linking to original path
156             # res => The response from the request for this page
157             # redirect_to => The redirect location, if it was a redirect
158 8         384 my %history;
159              
160 8         33 while ( my $page = shift @pages ) {
161 40 50       131 next if $history{ $page }{ res };
162 40         137 my $tx = $ua->get( $page );
163 40         503845 my $res = $tx->res;
164              
165             # Do not try to write error messages
166 40 100       242 if ( $res->is_error ) {
167 5 50       145 if ( !$opt->{quiet} ) {
168 5         22 say sprintf " [ERROR] %s - %s %s",
169             $page, $res->code, $res->message;
170             }
171 5         278 next;
172             }
173              
174             # Rewrite links to redirects
175 35 100       655 if ( $res->is_redirect ) {
176 5         80 my $loc = $history{ $page }{ redirect_to } = $res->headers->location;
177 5         95 for my $link_from ( keys %{ $history{ $page }{ link_from } } ) {
  5         29  
178 5         13 for my $el ( @{ $history{ $page }{ link_from }{ $link_from } } ) {
  5         17  
179 5         29 $el->attr( href => $loc );
180             }
181             }
182 5         164 next;
183             }
184              
185 30         437 my $type = $res->headers->content_type;
186 30 100 66     680 if ( $type and $type =~ m{^text/html} and my $dom = $res->dom ) {
      66        
187 25         15113 my $dir = path( $page )->dirname;
188 25         1462 for my $attr ( qw( href src ) ) {
189 50         1674 for my $el ( $dom->find( "[$attr]" )->each ) {
190 58         10933 my $url = $el->attr( $attr );
191              
192             # Don't analyze full URLs
193 58 100       1080 next if $url =~ m{^(?:[a-zA-Z]+:|//)};
194             # Don't analyze in-page fragments
195 42 100       106 next if $url =~ m{^#};
196              
197             # Fix relative paths
198 37 100       138 my $path = $url =~ m{^/} ? $url : $dir->child( $url )."";
199             # Remove fragment
200 37         396 $path =~ s/#.+//;
201              
202 37 50       138 if ( my $loc = $history{ $path }{ redirect_to } ) {
203 0         0 $el->attr( $attr => $loc );
204 0         0 next;
205             }
206             else {
207 37         61 push @{ $history{ $path }{ link_from }{ $page } }, $el;
  37         144  
208             }
209              
210 37 100       130 if ( !$history{ $path }{ res } ) {
211 32         154 push @pages, $path;
212             }
213              
214             }
215             }
216             }
217              
218 30         3842 $history{ $page }{ res } = $res;
219             }
220              
221             # Event for checking the status of everything we're about to export.
222             # We do this before rewriting the base URLs to make it easier to
223             # check for broken links.
224             my %to_export =
225 30 100       525 map { $_->[0] => $_->[1] =~ m{^text/html} ? $_->[2]->dom : $_->[2]->body }
226 30         416 map { [ $_, $history{ $_ }{ res }->headers->content_type, $history{ $_ }{ res } ] }
227 8         53 grep { $history{ $_ }{ res } }
  40         106  
228             keys %history;
229              
230 8         232 $self->emit( before_write => \%to_export );
231              
232 8         337 for my $page ( keys %to_export ) {
233 31         3132 my $content = $to_export{ $page };
234 31 100       125 if ( ref $content eq 'Mojo::DOM' ) {
235 25         83 my $dir = path( $page )->dirname;
236 25         1285 for my $attr ( qw( href src ) ) {
237 50         1427 for my $el ( $content->find( "[$attr]" )->each ) {
238 58         10134 my $url = $el->attr( $attr );
239             # Rewrite absolute paths
240 58 100 100     1050 if ( $opt->{base} && $url =~ m{^/} ) {
241 5 100       49 my $base_url = $url eq '/' ? $opt->{base} : $opt->{base} . $url;
242 5         14 $el->attr( $attr => $base_url );
243             }
244             }
245             }
246             }
247 31         3664 $self->_write( $root, $page, $content, $opt->{quiet} );
248             }
249             }
250              
251             sub _write {
252 31     31   96 my ( $self, $root, $page, $content, $quiet ) = @_;
253 31 100       101 if ( ref $content eq 'Mojo::DOM' ) {
254             # Mojolicious automatically decodes using the response content
255             # type, so all we need to do is encode it into the file content
256             # type that we want
257             # TODO: Allow configuring the destination encoding
258             # TODO: Ensure all text/* MIME types use the destination
259             # encoding
260 25         82 $content = encode 'utf8', $content;
261             }
262 31         5352 my $to = $root->child( $page );
263 31 100       695 if ( $to !~ m{[.][^/.]+$} ) {
264 25         230 $to = $to->child( 'index.html' );
265             }
266              
267 31         464 my $dir = $to->dirname;
268 31 100       1173 if ( !-d $dir ) {
269 12         371 $dir->make_path;
270 12 100       2316 say " [mkdir] $dir" unless $quiet;
271             }
272             else {
273 19 100       465 say " [exist] $dir" unless $quiet;
274             }
275              
276 31 100       1370 say " [write] $to" unless $quiet;
277 31         432 $to->spurt( $content );
278             }
279              
280             1;
281              
282             __END__