File Coverage

blib/lib/RapidApp/Template/Provider.pm
Criterion Covered Total %
statement 48 103 46.6
branch 9 56 16.0
condition 3 9 33.3
subroutine 13 20 65.0
pod 0 7 0.0
total 73 195 37.4


line stmt bran cond sub pod time code
1             package RapidApp::Template::Provider;
2 4     4   29 use strict;
  4         7  
  4         109  
3 4     4   20 use warnings;
  4         7  
  4         87  
4 4     4   18 use autodie;
  4         9  
  4         129  
5              
6 4     4   18282 use RapidApp::Util qw(:all);
  4         9  
  4         1775  
7 4     4   28 use Path::Class qw(file dir);
  4         6  
  4         243  
8 4     4   1762 use RapidApp::Template::Access::Dummy;
  4         14  
  4         148  
9              
10 4     4   32 use Moo;
  4         11  
  4         17  
11 4     4   10407 use Types::Standard ':all';
  4         11  
  4         68  
12             extends 'Template::Provider';
13              
14 4     4   157114 use Module::Runtime;
  4         12  
  4         37  
15              
16             =pod
17              
18             =head1 DESCRIPTION
19              
20             Base Template Provider class with extended API for updating templates. Extends L<Template::Provider>
21             and, like that class, works with filesystem based templates, including updating of filesystem
22             templates. Designed specifically to work with RapidApp::Template::Controller.
23              
24             =cut
25              
26             # The RapidApp::Template::Controller instance
27             has 'Controller', is => 'ro', required => 1;
28              
29             # The RapidApp::Template::Access instance:
30             # We need to be able to check certain template permissions for special markup
31             # Actual permission checks happen in the RapidApp::Template::Controller
32             has 'Access', is => 'ro', required => 1;
33              
34             has 'store_class', is => 'ro', default => sub {undef}; # Will be 'RapidApp::Template::Store' if undef
35             has 'store_params', is => 'ro', isa => Maybe[HashRef], default => sub {undef};
36             has 'Store', is => 'ro', lazy => 1, default => sub {
37             my $self = shift;
38            
39             # Support an 'AccessStore' which means the Access class is both an Access and a Store at once.
40             # This gives the flexibility to design for a single interface, or separately. Currently this
41             # requires MI, but may change this to Roles...
42             return $self->Access if ($self->Access->isa('RapidApp::Template::Store'));
43            
44             my $class = $self->store_class || 'RapidApp::Template::Store';
45             Module::Runtime::require_module($class);
46             $class->new({ Provider => $self, %{ $self->store_params||{} } });
47             }, isa => InstanceOf['RapidApp::Template::Store'];
48              
49             sub _store_owns_template {
50 32     32   52 my ($self, $name) = @_;
51 32 50       69 return 0 if ($self->{IGNORE_STORE_OWNERSHIP});
52 32   66     132 $self->{_store_owns_template}{$name} //= do { # Only ask the Store if it owns a template once
53 2 50       48 $name =~ /^\//
54             ? 0 # never ask about absolute paths
55             : $self->Store->owns_tpl($name)
56             }
57             }
58              
59             # -------
60             # The "DummyAccess" API is a quick/dirty way to turn off all access checks
61             # This was added after the fact to be able to safely render templates outside
62             # of "request" context without requiring the Access API to handle situations
63             # where $c is null. See new template_render() method in Template::Controller
64             has '_DummyAccess', is => 'ro', lazy => 1, default => sub {
65             my $self = shift;
66             return RapidApp::Template::Access::Dummy->new({
67             Controller => $self->Controller
68             });
69             };
70             around 'Access' => sub {
71             my ($orig,$self) = @_;
72             return $self->Controller->{_dummy_access}
73             ? $self->_DummyAccess
74             : $self->$orig
75             };
76             # -------
77              
78             # Whether or not to wrap writable templates in a special <div> tag for target/selection
79             # in JavaScript client (for creating edit selector/tool GUI)
80             has 'div_wrap', is => 'ro', default => sub{0};
81              
82             # This only applies to filesystem-based templates and when creatable templates are enabled:
83             has 'new_template_path', is => 'ro', lazy => 1, default => sub{
84             my $self = shift;
85             # default to the first include path
86             my $paths = $self->paths or die "paths() didn't return a true value";
87             return $paths->[0];
88             };
89              
90             around 'fetch' => sub {
91             my ($orig, $self, $name) = @_;
92            
93             return $self->$orig($name) if (ref $name eq 'SCALAR');
94            
95             $name = $self->Controller->_resolve_template_name($name);
96            
97             # Save the template fetch name:
98             local $self->{template_fetch_name} = $name;
99             return $self->$orig($name);
100             };
101              
102              
103             around 'load' => sub {
104             my ($orig, $self, $name) = @_;
105            
106             return $self->$orig($name) unless ($self->_store_owns_template($name));
107            
108             return $self->Store->template_exists($name)
109             ? ( $self->Store->template_content($name), Template::Constants::STATUS_OK )
110             : ( "Template '$name' not found", Template::Constants::STATUS_ERROR )
111             };
112              
113              
114             around '_template_modified' => sub {
115             my ($orig, $self, $name) = @_;
116             my $template = $self->{template_fetch_name} || $name;
117            
118             my ($exists,$modified);
119             if($self->_store_owns_template($template)) {
120             $exists = $self->Store->template_exists($template);
121             $modified = $self->Store->template_mtime($template) if ($exists);
122             }
123             else {
124             $modified = $self->$orig($name);
125             $exists = $self->template_exists($template) unless ($self->{template_exists_call});
126             }
127            
128             # Need to return a virtual value to enable the virtual content for
129             # creating non-extistent templates
130             $modified = 1 if (
131             ! $modified && ! $exists &&
132             ! $self->{template_exists_call} && #<-- localized in template_exists() below
133             $self->Access->template_creatable($template)
134             );
135            
136             return $modified;
137             };
138              
139             # Wraps writable templates with a div (if enabled)
140             around '_template_content' => sub {
141             my ($orig, $self, @args) = @_;
142            
143             my $template = $self->{template_fetch_name} || join('/',@args);
144            
145             my ($data, $error, $mod_date);
146            
147             if ($self->template_exists($template)) {
148             return $self->$orig(@args) unless ($self->_store_owns_template($template));
149             # Proxy to the Store to return the content
150             ($data, $error, $mod_date) = (
151             $self->Store->template_content ($template), undef,
152             $self->Store->template_mtime ($template)
153             );
154             }
155             else {
156             # Return virtual non-existent content, optionally with markup
157             # to enable on-the-fly creating the template:
158             ($data, $error, $mod_date) = (
159             $self->_not_exist_content(
160             $template,
161             ($self->div_wrap && $self->Access->template_creatable($template))
162             ), undef, 1
163             );
164             }
165            
166             return wantarray
167             ? ( $data, $error, $mod_date )
168             : $data;
169             };
170              
171              
172              
173             sub _not_exist_content {
174 0     0   0 my ($self, $template,$creatable) = @_;
175            
176 0 0       0 my $inner = $creatable
177             ? 'Template <span class="tpl-name">' . $template . '</span> doesn\'t exist yet' .
178             '<div title="Create \'' . $template . '\'" class="create with-icon ra-icon-selection-add">Create Now</div>'
179             : 'Template <span class="tpl-name">' . $template . '</span> doesn\'t exist';
180            
181 0 0       0 my $outer = $creatable
182             ? '<div class="not-exist creatable">' . $inner . '</div>'
183             : '<div class="not-exist">' . $inner . '</div>';
184            
185 0         0 return join("\n",
186             '<div class="ra-template">',
187            
188             '<div class="meta" style="display:none;">',
189             #'<div class="template-name">', $template, '</div>',
190             encode_json_utf8({
191             name => $template,
192             format => $self->Access->get_template_format($template)
193             }),
194             '</div>',
195            
196             $outer,
197            
198             '</div>'
199             );
200             }
201              
202             # ---
203             # Override to decode everything as UTF-8. If there are templates in some
204             # other encoding, it is probably a mistake, and even if its not, the rest of
205             # the system won't be able to deal with it properly anyway. UTF-8 is
206             # currently is assumed across-the-board in RapidApp.
207             sub _decode_unicode {
208 2     2   281 my ($self, $string) = @_;
209 2 50       8 return undef unless (defined $string);
210 2         32 utf8::decode($string);
211 2         6 return $string;
212             }
213             # ---
214              
215             ###
216             ### Over and above the methods in the Template::Provider API:
217             ###
218              
219              
220             # Simple support for writing to filesystem-based templates to match the
221             # default Template::Provider for reading filesystem-based templates. Note
222             # that the permission check happens in the RapidApp::Template::Controller,
223             # before this method is called.
224             sub update_template {
225 0     0 0 0 my ($self, $template, $content) = @_;
226            
227 0 0       0 return $self->Store->update_template($template,$content)
228             if $self->_store_owns_template($template);
229            
230 0         0 my $path = $self->get_template_path($template);
231 0         0 my $File = file($path);
232            
233 0 0       0 die "Bad template path '$File'" unless (-f $File);
234            
235 0         0 return $File->spew(iomode => '>:raw', $content);
236             }
237              
238             sub template_exists {
239 8     8 0 19 my ($self, $template) = @_;
240 8         20 local $self->{template_exists_call} = 1;
241 8 50       24 return $self->_store_owns_template($template)
    50          
242             ? $self->Store->template_exists($template)
243             : $self->get_template_path($template) ? 1 : 0;
244             }
245              
246             sub template_exists_locally {
247 0     0 0 0 my $self = shift;
248 0         0 local $self->{IGNORE_STORE_OWNERSHIP} = 1;
249 0         0 $self->template_exists(@_)
250             }
251              
252             # Copied from Template::Provider::load
253             sub get_template_path {
254 8     8 0 14 my ($self, $name) = @_;
255 8         13 my ($data, $error);
256 8         14 my $path = $name;
257            
258 8 50       79 if (File::Spec->file_name_is_absolute($name)) {
    50          
259             # absolute paths (starting '/') allowed if ABSOLUTE set
260             $error = "$name: absolute paths are not allowed (set ABSOLUTE option)"
261 0 0       0 unless $self->{ ABSOLUTE };
262             }
263             elsif ($name =~ m[$Template::Provider::RELATIVE_PATH]o) {
264             # anything starting "./" is relative to cwd, allowed if RELATIVE set
265             $error = "$name: relative paths are not allowed (set RELATIVE option)"
266 0 0       0 unless $self->{ RELATIVE };
267             }
268             else {
269             INCPATH: {
270             # otherwise, it's a file name relative to INCLUDE_PATH
271 8   50     13 my $paths = $self->paths()
  8         34  
272             || return ($self->error(), Template::Constants::STATUS_ERROR);
273            
274 8         203 foreach my $dir (@$paths) {
275 16         136 $path = File::Spec->catfile($dir, $name);
276             last INCPATH
277 16 100       313 if $self->_template_modified($path);
278             }
279 0         0 undef $path; # not found
280             }
281             }
282              
283             #######
284              
285 8         34 return $path;
286             }
287              
288             sub create_template {
289 0     0 0   my ($self, $template, $content) = @_;
290            
291             # TODO: formalize a way to dynamically set/specify the new/init content
292 0 0         $content = "New Template '$template'" unless (defined $content);
293            
294 0 0         return $self->Store->create_template($template,$content)
295             if $self->_store_owns_template($template);
296            
297 0           my $File = file($self->new_template_path,$template);
298 0 0         die "create_templete(): ERROR - $File already exists!" if (-f $File);
299            
300 0           my $Dir = $File->parent;
301 0 0         unless (-d $Dir) {
302 0 0         $Dir->mkpath or die "create_templete(): mkpath failed for '$Dir'";
303             }
304            
305 0           $File->spew(iomode => '>:raw', $content);
306            
307 0 0         return -f $File ? 1 : 0;
308             }
309              
310             sub delete_template {
311 0     0 0   my ($self, $template) = @_;
312            
313 0 0         return $self->Store->delete_template($template) if $self->_store_owns_template($template);
314            
315 0           my $File = file($self->get_template_path($template));
316 0 0         die "delete_templete(): ERROR - $File doesn't exist or is not a regular file"
317             unless (-f $File);
318            
319 0 0         unlink($File) or die "delete_templete(): unlink failed for '$File'";
320            
321 0 0         return -f $File ? 0 : 1;
322             }
323              
324              
325             sub list_templates {
326 0     0 0   my ($self, @regexes) = @_;
327            
328 0           my @re = map { qr/$_/ } @regexes;
  0            
329 0           my @files = ();
330            
331 0           my $paths = $self->{INCLUDE_PATH};
332 0 0         $paths = [$paths] unless (ref $paths);
333            
334 0           my %seen = ();
335 0           for my $dir (grep { -d $_ } map { dir($_) } @$paths) {
  0            
  0            
336             $dir->recurse(
337             preorder => 1,
338             depthfirst => 1,
339             callback => sub {
340 0     0     my $child = shift;
341 0 0         return if ($child->is_dir);
342 0           my $tpl = $child->relative($dir)->stringify;
343            
344             # If regex(es) were supplied, check that the template matches
345             # all of them
346 0   0       !($tpl =~ $_) and return for (@re);
347            
348             ## If regex(es) were supplied, check that the template matches
349             ## at least *one* of them
350             #if(scalar(@re) > 0) {
351             # my $m = 0;
352             # for my $r (@re) {
353             # $m++ if ($tpl =~ $r);
354             # last if ($m);
355             # }
356             # return unless ($m > 0);
357             #}
358            
359             # Make sure we include the same physical template only once:
360 0 0         return if ($seen{$child->absolute->stringify}++);
361            
362 0           push @files, $tpl;
363             }
364 0           );
365             }
366            
367             push @files, grep {
368 0           my ($name,$incl) = ($_, 1);
369 0   0       !($name =~ $_) and $incl = 0 for (@re);
370 0           $incl
371 0 0         } @{ $self->Store->list_templates(@regexes) || [] };
  0            
372            
373 0           return \@files;
374             }
375              
376              
377             1;