File Coverage

blib/lib/RapidApp/Template/Controller.pm
Criterion Covered Total %
statement 114 278 41.0
branch 11 126 8.7
condition 4 68 5.8
subroutine 30 51 58.8
pod 0 18 0.0
total 159 541 29.3


line stmt bran cond sub pod time code
1             package RapidApp::Template::Controller;
2 4     4   2773 use strict;
  4         10  
  4         117  
3 4     4   19 use warnings;
  4         7  
  4         129  
4              
5 4     4   20 use RapidApp::Util qw(:all);
  4         8  
  4         1820  
6 4     4   26 use Try::Tiny;
  4         9  
  4         225  
7 4     4   23 use Template;
  4         8  
  4         90  
8 4     4   24 use Module::Runtime;
  4         7  
  4         29  
9 4     4   143 use Path::Class qw(file dir);
  4         9  
  4         197  
10 4     4   24 use URI::Escape;
  4         9  
  4         180  
11              
12             # New unified controller for displaying and editing TT templates on a site-wide
13             # basis. This is an experiment that breaks with the previous RapidApp 'Module'
14             # design. It also is breaking away from DataStore2 for editing in order to support
15             # nested templates (i.e. tree structure instead of table/row structure)
16              
17 4     4   21 use Moose;
  4         7  
  4         33  
18             with 'RapidApp::Role::AuthController';
19 4     4   22768 use namespace::autoclean;
  4         10  
  4         35  
20              
21 4     4   276 BEGIN { extends 'Catalyst::Controller' }
22              
23 4     4   32567 use RapidApp::Template::Context;
  4         12  
  4         129  
24 4     4   2140 use RapidApp::Template::Provider;
  4         15  
  4         145  
25 4     4   30 use RapidApp::Template::Access;
  4         9  
  4         5277  
26              
27             ## -----
28             ## Setup Top-level alias URL paths:
29             has 'read_alias_path', is => 'ro', default => '/tpl';
30             has 'edit_alias_path', is => 'ro', default => '/tple';
31              
32             sub BUILD {
33 4     4 0 10151 my $self = shift;
34            
35 4         33 my $c = $self->_app;
36 4         150 my $ns = $self->action_namespace($c);
37            
38 4 50       1133 if($self->read_alias_path) {
39 4         98 my $path = $self->read_alias_path;
40 4         20 $path =~ s/^\///;
41              
42 4         35 $c->dispatcher->register( $c => Catalyst::Action->new({
43             name => 'tpl',
44             code => $self->can('tpl'),
45             class => $self->meta->name,
46             namespace => $ns,
47             reverse => join('/',$ns,'tpl'),
48             attributes => {
49             Chained => ['/'],
50             PathPrefix => [''],
51             }
52             }));
53             }
54            
55 4 50       206066 if($self->edit_alias_path) {
56 4         101 my $path = $self->edit_alias_path;
57 4         18 $path =~ s/^\///;
58 4         44 $c->dispatcher->register( $c => Catalyst::Action->new({
59             name => 'tple',
60             code => $self->can('tple'),
61             class => $self->meta->name,
62             namespace => $ns,
63             reverse => join('/',$ns,'tple'),
64             attributes => {
65             Chained => ['/'],
66             PathPrefix => [''],
67             }
68             }));
69             }
70             }
71              
72              
73              
74             sub tpl_path {
75 0     0 0 0 my $self = shift;
76             # Return the edit alias patgh first or fall back to the read alias:
77 0   0     0 return $self->edit_alias_path || $self->read_alias_path;
78             }
79             ## -----
80              
81              
82             has 'context_class', is => 'ro', default => 'RapidApp::Template::Context';
83             has 'provider_class', is => 'ro', default => 'RapidApp::Template::Provider';
84             has 'access_class', is => 'ro', default => 'RapidApp::Template::Access';
85             has 'access_params', is => 'ro', isa => 'HashRef', default => sub {{}};
86             has 'include_paths', is => 'ro', isa => 'ArrayRef[Str]', default => sub {[]};
87             has 'store_class', is => 'ro', default => sub {undef}; # Will be 'RapidApp::Template::Store' if undef
88             has 'store_params', is => 'ro', default => sub {undef};
89              
90              
91             # --
92             # Only these TT plugins and filters will be allowed in Templates
93             # This is *very* important for security if non-admin users will
94             # have access to modify templates. These templates and filters
95             # are from the built-in list and are known to be safe. Examples
96             # of unsafe plugins are things like 'Datafile' and 'DBI', and
97             # examples of unsafe filters are things like 'eval', 'redirect', etc.
98             # It is critical that these types of things are never exposed
99             # to web interfaces. It is also important to note that TT was
100             # not originally designed with "limited" access in mind - it was
101             # only meant to be used by the programmer, not users. In hindsight,
102             # it might have been better to go with Text::Xslate for this reason
103             #
104             # (note: these get accessed/used within RapidApp::Template::Context)
105             has 'allowed_plugins', is => 'ro', default => sub {[qw(
106             Assert Date Dumper Format HTML Iterator
107             Scalar String Table URL Wrap
108             )]}, isa => 'ArrayRef';
109              
110             has 'allowed_filters', is => 'ro', default => sub {[qw(
111             format upper lower ucfirst lcfirst trim collapse
112             html html_entity xml html_para html_break
113             html_para_break html_line_break uri url indent
114             truncate repeat remove replace null
115             )]}, isa => 'ArrayRef';
116             # --
117              
118             has 'default_template_extension', is => 'ro', isa => 'Maybe[Str]', default => 'tt';
119              
120             # If true, mouse-over edit controls will always be available for editable
121             # templates. Otherwise, query string ?editable=1 is required. Note that
122             # editable controls are *only* available in the context of an AutoPanel tab
123             has 'auto_editable', is => 'ro', isa => 'Bool', default => 0;
124              
125             has 'Access', is => 'ro', lazy => 1, default => sub {
126             my $self = shift;
127             Module::Runtime::require_module($self->access_class);
128             return $self->access_class->new({
129             %{ $self->access_params },
130             Controller => $self
131             });
132             }, isa => 'RapidApp::Template::Access';
133              
134             # Maintain two separate Template instances - one that wraps divs and one that
135             # doesn't. Can't use the same one because compiled templates are cached
136             has 'Template_raw', is => 'ro', lazy => 1, default => sub {
137             my $self = shift;
138             return $self->_new_Template({ div_wrap => 0 });
139             }, isa => 'Template';
140              
141             has 'Template_wrap', is => 'ro', lazy => 1, default => sub {
142             my $self = shift;
143             return $self->_new_Template({ div_wrap => 1 });
144             }, isa => 'Template';
145              
146             sub _new_Template {
147 1     1   5 my ($self,$opt) = @_;
148 1         34 Module::Runtime::require_module($self->context_class);
149 1         55 Module::Runtime::require_module($self->provider_class);
150             return Template->new({
151             CONTEXT => $self->context_class->new({
152             Controller => $self,
153             Access => $self->Access,
154             # TODO: turn STRICT back on once I figure out how to make the errors useful:
155             #STRICT => 1,
156             LOAD_TEMPLATES => [
157             $self->provider_class->new({
158             Controller => $self,
159             Access => $self->Access,
160             store_class => $self->store_class,
161             store_params => $self->store_params,
162             #INCLUDE_PATH => $self->_app->default_tt_include_path,
163             INCLUDE_PATH => [
164 2         262 map { dir($_)->stringify } @{ $self->include_paths },
  1         27  
165             dir($self->_app->config->{home},'root/templates')->stringify,
166             dir(RapidApp->share_dir,'templates')->stringify
167             ],
168             CACHE_SIZE => 64,
169 1 50       44 %{ $opt || {} }
  1         61  
170             })
171             ]
172             })
173             })
174             }
175              
176             # hook for RapidApp::Role::AuthController:
177             # abort enforce_session for 'external' templates being accessed externally:
178             around 'enforce_rapidapp_session' => sub {
179             my ( $orig, $self, $c, @args ) = @_;
180             my $template = join('/',@{$c->req->args});
181             return $self->$orig($c,@args) unless (
182             ! $c->req->header('X-RapidApp-RequestContentType')
183             && $self->is_external_template($c,$template)
184             );
185             };
186              
187             sub get_Provider {
188 1     1 0 3 my $self = shift;
189 1         43 return $self->Template_raw->context->{LOAD_TEMPLATES}->[0];
190             }
191              
192             # request lifetime cached:
193             sub _template_exists {
194 0     0   0 my ($self, $c, $template) = @_;
195 0 0       0 die "missing template arg" unless ($template);
196             $c->stash->{_template_exists}{$template} =
197             $self->get_Provider->template_exists($template)
198 0 0       0 unless (exists $c->stash->{_template_exists}{$template});
199 0         0 return $c->stash->{_template_exists}{$template};
200             }
201              
202              
203             # Checks if the editable toggle/switch is on for this request. Note that
204             # this has *nothing* to do with actual editability of a given template,
205             # just whether or not edit controls should be available for templates that
206             # are allowed to be edited
207             sub is_editable_request {
208 0     0 0 0 my ($self, $c) = @_;
209            
210             # Never editable externally, unless this is an iframe request
211             return 0 unless (
212             $c->req->header('X-RapidApp-RequestContentType') ||
213 0 0 0     0 (exists $c->req->params->{iframe} && $c->req->params->{iframe} eq 'request')
      0        
214             );
215            
216             # check several mechanisms to turn on editing (mouse-over edit controls)
217             return (
218             $self->auto_editable ||
219             $c->req->params->{editable} ||
220             $c->req->params->{edit} ||
221             $c->stash->{editable}
222 0   0     0 );
223             }
224              
225             sub is_iframe_request {
226 0     0 0 0 my ($self, $c) = @_;
227            
228             return (
229             $c->req->params->{iframe} ||
230             $c->stash->{iframe}
231 0   0     0 );
232             }
233              
234             # request lifetime cached:
235             sub is_external_template {
236 0     0 0 0 my ($self, $c, $template) = @_;
237 0 0       0 die "missing template arg" unless ($template);
238            
239             $c->stash->{is_external_template}{$template} = do {
240             # Allow params/stash override:
241 0 0       0 return $c->req->params->{external} if (exists $c->req->params->{external});
242 0 0       0 return $c->stash->{external} if (exists $c->stash->{external});
243            
244 0 0 0     0 my $external = (
245             # hard-coded external templates:
246             $template =~ /^rapidapp\/public\// ||
247             $self->Access->template_external_tpl($template)
248             ) ? 1 : 0;
249              
250             return (
251 0 0 0     0 $external &&
252             # don't treat non-existing templates as external
253             $self->_template_exists($c,$template)
254             ) ? 1 : 0;
255 0 0       0 } unless (exists $c->stash->{is_external_template}{$template});
256            
257 0         0 return $c->stash->{is_external_template}{$template};
258             }
259              
260              
261             sub _resolve_template_name {
262 4     4   11 my ($self, @args) = @_;
263 4 50       13 return undef unless (defined $args[0]);
264 4         24 my $template = join('/',@args);
265            
266 4 50 33     121 $template .= '.' . $self->default_template_extension if (
267             $self->default_template_extension &&
268             ! ( $template =~ /\./ ) #<-- doesn't contain a dot '.'
269             );
270            
271 4         15 return $template;
272             }
273              
274 4     4 0 33 sub base :Chained :PathPrefix :CaptureArgs(0) {}
  4     0   9  
  4         45  
275              
276             # -------
277             # direct/navable dispatch to match the same pattern as Module DirectCmp
278             #
279             # We now have 'direct' and 'navable' controller actions to that the following
280             # URL paths will behave is a consistent manner:
281             #
282             # /rapidapp/module/direct/<module_path>
283             # /rapidapp/template/direct/tple/<template_path>
284             #
285             # We are simulating module viewport content with a simple autopanel cfg so
286             # that a template can be rendered full-screen with the same functionality
287             # as it has when rendered in a TabGui tab
288             sub _validate_args_template_viewable {
289 0     0   0 my ($self, @args) = @_;
290 0         0 my $template = $self->_resolve_template_name(@args);
291 0         0 $self->get_Provider->template_exists($template)
292             }
293              
294             sub _validate_enforce_arguments_path {
295 0     0   0 my ($self, $c, @args) = @_;
296            
297 0 0 0     0 shift @args if ($args[0] eq 'tpl' || $args[0] eq 'tple');
298 0 0       0 return 1 if ($self->_validate_args_template_viewable(@args));
299            
300 0         0 my @pre_args = ();
301 0         0 while(scalar(@args) > 1) {
302            
303 0         0 push @pre_args, shift @args;
304            
305             # Special handling for relative requests to special/reserved controller paths.
306             # this is based on the same logic/rules as in RapidApp::Module
307 0 0 0     0 $c->redispatch_public_path($c->mount_url,@args) && $c->detach if (
      0        
      0        
308             $self->_validate_args_template_viewable(@pre_args) && (
309             $args[0] eq 'simplecas'
310             || $args[0] eq 'assets'
311             || $args[0] eq 'rapidapp'
312             )
313             );
314             }
315            
316 0         0 $c->stash->{template} = 'rapidapp/http-404.html';
317 0         0 $c->stash->{current_view} = 'RapidApp::Template';
318 0         0 $c->res->status(404);
319 0         0 return $c->detach;
320             }
321              
322             sub direct :Chained('base') :Args {
323 0     0 0 0 my ($self, $c, @args) = @_;
324            
325 0         0 $self->_validate_enforce_arguments_path($c,@args);
326            
327             $c->stash->{panel_cfg} = {
328 0         0 xtype => 'autopanel',
329             layout => 'fit',
330             autoLoad => {
331             url => join('/','',@args),
332             params => $c->req->params
333             }
334             };
335            
336 0         0 return $c->detach( $c->view('RapidApp::Viewport') );
337 4     4   6484 }
  4         11  
  4         15  
338              
339             sub navable :Chained('base') :Args {
340 0     0 0 0 my ($self, $c, @args) = @_;
341 0         0 $self->_validate_enforce_arguments_path($c,@args);
342 0         0 return $c->controller('RapidApp::Module')->navable($c,@args);
343 4     4   3515 }
  4         11  
  4         17  
344             # --------
345              
346             sub tpl :Chained('base') :Args {
347 0     0 0 0 my ($self, $c, @args) = @_;
348 0         0 $self->view($c,@args)
349 4     4   3333 }
  4         10  
  4         25  
350              
351             # Edit alias
352             sub tple :Chained('base') :Args {
353 0     0 0 0 my ($self, $c, @args) = @_;
354            
355 0         0 $c->stash->{editable} = 1;
356 0         0 $self->view($c,@args)
357 4     4   3364 }
  4         10  
  4         18  
358              
359             # TODO: see about rendering with Catalyst::View::TT or a custom View
360             sub view :Chained('base') :Args {
361 0     0 0 0 my ($self, $c, @args) = @_;
362 0 0       0 my $template = $self->_resolve_template_name(@args)
363             or die "No template specified";
364            
365 0         0 local $self->Access->{_local_cache} = {};
366            
367 0 0       0 if(my $psgi_response = $self->Access->template_psgi_response($template,$c)) {
368 0         0 $c->res->from_psgi_response( $psgi_response );
369 0         0 return $c->detach;
370             }
371              
372 0         0 my $ra_client = $c->is_ra_ajax_req;
373              
374             # Honor the existing status, if set, except for Ajax requests
375 0   0     0 my $status = $c->res->status || 200;
376 0 0       0 $status = 200 if ($ra_client);
377              
378 0         0 local $self->{_current_context} = $c;
379            
380             # Track the top-level template that is being viewed, in case the Access class
381             # wants to treat top-level templates differently from nested templates
382             # -- see currently_viewing_template() in RapidApp::Template::Access
383 0         0 local $self->{_viewing_template} = $template;
384            
385 0 0       0 $self->Access->template_viewable($template)
386             or die "Permission denied - template '$template'";
387              
388 0         0 my $external = $self->is_external_template($c,$template);
389 0         0 my $editable = $self->is_editable_request($c);
390            
391             # ---
392             # New: for non-external templates which are being accessed externally,
393             # (i.e. directly from browser) redirect to internal hashnav path:
394 0 0 0     0 unless ($external || $ra_client) {
395 0         0 return $c->auto_hashnav_redirect_current;
396             }
397             #---
398            
399 0   0     0 my $iframe = $external || $self->is_iframe_request($c); # <-- external must use iframe
400 0         0 my ($output,$content_type);
401            
402 0         0 $content_type = $self->Access->template_content_type($template);
403            
404 0         0 my @cls = ('ra-scoped-reset');
405 0         0 my $tpl_cls = $self->Access->template_css_class($template);
406 0 0       0 push @cls, $tpl_cls if ($tpl_cls);
407            
408 0 0       0 if($ra_client) {
409             # This is a call from within ExtJS, wrap divs to id the templates from javascript
410            
411 0         0 my $cnf = {};
412            
413 0 0       0 if($iframe) {
414             # This is an iframe request. Build an iframe panel which will make the request
415             # again but without the X-RapidApp-RequestContentType header which will be
416             # handled as a direct browser request (see logic further down)
417            
418 0         0 my %params = ( %{$c->req->params}, editable => $editable, iframe => 'request' );
  0         0  
419 0         0 my $qs = join('&',map { $_ . '=' . uri_escape($params{$_}) } keys %params);
  0         0  
420 0         0 my $iframe_src = join('/','',$self->action_namespace($c),'view',$template) . '?' . $qs;
421              
422 0         0 $cnf = {
423             xtype => 'iframepanel',
424             plugins => ['ra-link-click-catcher'],
425             tabTitle => join('',
426             '<span style="color:purple;">',
427             '[' . join('/',@args) . ']', #<-- not using $template to preserve the orig req name
428             '</span>'
429             ),
430             tabIconCls => 'ra-icon-page-white',
431             style => 'top: 0; left: 0; bottom: 0; right: 0;',
432             autoScroll => \1,
433             bodyStyle => 'border: 1px solid #D0D0D0;background-color:white;',
434             loadMask => \1,
435             defaultSrc => $iframe_src
436             };
437             }
438             else {
439            
440 0 0       0 my $html = $self->_render_template(
441             $editable ? 'Template_wrap' : 'Template_raw',
442             $template, $c
443             );
444            
445             $cnf = {
446             xtype => 'panel',
447             autoScroll => \1,
448             bodyCssClass => join(' ',@cls),
449            
450             # try to set the title/icon by finding/parsing <title> in the 'html'
451             autopanel_parse_title => \1,
452            
453             # These will only be the title/icon if there is no parsable <title>
454             tabTitle => join('/',@args), #<-- not using $template to preserve the orig req name
455             tabIconCls => 'ra-icon-page-white-world',
456            
457             html => $html,
458            
459             # Load any extra, template-specific configs from the Access class:
460 0 0       0 %{ $self->Access->template_autopanel_cnf($template) || {} }
  0         0  
461             };
462             }
463            
464             # No reason to load the plugin unless we're editable:
465 0 0       0 if ($editable) {
466 0   0     0 $cnf->{plugins} ||= [];
467 0         0 push @{$cnf->{plugins}}, 'template-controller-panel';
  0         0  
468 0         0 $cnf->{template_controller_url} = '/' . $self->action_namespace($c);
469             }
470            
471             # This is doing the same thing that the overly complex 'Module' controller does:
472 0         0 $content_type = 'text/javascript; charset=utf-8';
473 0         0 $output = encode_json_utf8($cnf);
474             }
475             else {
476             # This is a direct browser call:
477            
478 0 0       0 my $html = $self->_render_template(
479             $editable ? 'Template_wrap' : 'Template_raw',
480             $template, $c
481             );
482            
483 0         0 my @head = ();
484            
485             # If we're in an iframe tab, we want to make sure we set the base target
486             # to prevent the chance of trying to load a link inside the frame (even
487             # though local links are already hanlded/converted - we still need to
488             # protect against external/global links).
489             push @head, '<base target="_blank" />' if (
490             exists $c->req->params->{iframe} &&
491 0 0 0     0 $c->req->params->{iframe} eq 'request'
492             );
493            
494 0 0       0 if($external) {
495            
496             # Ask the Access class for custom headers for this external template, and
497             # if it has them, set them in the response object now. And pull out $content_type
498             # if this operation set it (i.e. the template provides its own Content-Type)
499             # NEW: the template can also now provide its content type via ->template_content_type
500             # which takes priority and is the preferred API
501 0 0       0 my $headers = $external ? $self->Access->get_external_tpl_headers($template) : undef;
502 0 0       0 if($headers) {
503 0         0 $c->res->header( $_ => $headers->{$_} ) for (keys %$headers);
504 0   0     0 $content_type ||= $c->res->content_type;
505             }
506            
507             # If this external template provides its own headers, including Content-Type, and that is
508             # *not* text/html, don't populate @head, even if it is $editable (which is rare - or maybe
509             # even impossible - here anyway)
510 0 0 0     0 unless($content_type && ! ($content_type =~ /^text\/html/)){
511             # If we're editable and external we need to include CSS for template edit controls:
512             # TODO: basically including everything but ExtJS CSS. This is ugly and should
513             # be generalized/available in the Asset system as a simpler function call:
514 0 0 0     0 push @head, (
515             $c->favicon_head_tag||'',
516             $c->controller('Assets::RapidApp::CSS')->html_head_tags,
517             $c->controller('Assets::RapidApp::Icons')->html_head_tags,
518             $c->controller('Assets::ExtJS')->html_head_tags( js => [
519             'adapter/ext/ext-base.js',
520             'ext-all-debug.js',
521             'src/debug.js'
522             ]),
523             $c->controller('Assets::RapidApp::JS')->html_head_tags
524             ) if $editable; # $editable is rarely true here, so @header will be empty here anyway
525             }
526             }
527             else {
528             # Include all the ExtJS, RapidApp and local app CSS/JS
529 0         0 push @head, $c->all_html_head_tags;
530             }
531            
532             # Only include the RapidApp/ExtJS assets and wrap 'ra-scoped-reset' if
533             # this is *not* an external template. If it is an external template,
534             # ignore @head entirely if its empty:
535 0 0       0 $output = $external
    0          
536             ? ( scalar(@head) == 0 ? $html : join("\n",@head,$html) )
537             : join("\n",
538             '<head>', @head, '</head>',
539             '<div class="' . join(' ',@cls) . '">', $html, '</div>'
540             )
541             ;
542            
543             }
544            
545             # ----
546             # TODO/FIXME: back-compat force utf8 since removing code which sets content_type
547             # this is being done this way to match previous (pre 1.3105) code as close as possible
548             # this still needs to be revisited
549 0         0 utf8::decode($output);
550 0         0 $output = Encode::encode("UTF-8", $output);
551             # ----
552              
553 0         0 return $self->_detach_response($c,$status,$output,$content_type);
554 4     4   6241 }
  4         10  
  4         18  
555              
556              
557             # Read (not compiled/rendered) raw templates:
558             sub get :Chained('base') :Args {
559 0     0 0 0 my ($self, $c, @args) = @_;
560 0 0       0 my $template = $self->_resolve_template_name(@args)
561             or die "No template specified";
562            
563 0         0 local $self->{_current_context} = $c;
564            
565 0 0       0 $self->Access->template_readable($template)
566             or return $self->_detach_response($c,403,"Permission denied - template '$template'");
567            
568 0         0 my ($data, $error) = $self->get_Provider->load($template);
569            
570 0 0       0 return $self->_detach_response($c,400,"Failed to get template '$template'")
571             unless (defined $data);
572            
573             # Decode as UTF-8 for user consumption:
574 0         0 utf8::decode($data);
575            
576 0         0 return $self->_detach_response($c,200,$data);
577 4     4   3835 }
  4         8  
  4         19  
578              
579             # Update raw templates:
580             sub set :Chained('base') :Args {
581 0     0 0 0 my ($self, $c, @args) = @_;
582 0 0       0 my $template = $self->_resolve_template_name(@args)
583             or die "No template specified";
584            
585 0         0 local $self->{_current_context} = $c;
586            
587             exists $c->req->params->{content}
588 0 0       0 or return $self->_detach_response($c,400,"Template 'content' required");
589            
590 0 0       0 $self->Access->template_writable($template)
591             or return $self->_detach_response($c,403,"Modify template '$template' - Permission denied");
592            
593 0         0 my $content = $c->req->params->{content};
594            
595             # Special status 418 means the supplied content is a bad template
596 0 0       0 unless ($c->req->params->{skip_validate}) {
597 0         0 my $err = $self->_get_template_error('Template_raw',\$content,$c);
598 0 0       0 return $self->_detach_response($c,418,$err) if ($err);
599             }
600            
601             # Encode the template content in UTF-8
602 0         0 utf8::encode($content);
603            
604 0         0 $self->get_Provider->update_template($template,$content);
605            
606 0         0 return $self->_detach_response($c,200,'Template Updated');
607 4     4   3954 }
  4         9  
  4         19  
608              
609             sub create :Chained('base') :Args {
610 0     0 0 0 my ($self, $c, @args) = @_;
611 0 0       0 my $template = $self->_resolve_template_name(@args)
612             or die "No template specified";
613            
614 0         0 local $self->{_current_context} = $c;
615            
616 0 0       0 $self->Access->template_creatable($template)
617             or return $self->_detach_response($c,403,"Create template '$template' - Permission denied");
618            
619 0 0       0 die "Create template '$template' - already exists"
620             if $self->_template_exists($c,$template);
621            
622 0 0       0 $self->get_Provider->create_template($template)
623             or die "Failed to create template '$template'";
624              
625 0         0 return $self->_detach_response($c,200,"Created template '$template'");
626 4     4   3722 }
  4         9  
  4         19  
627              
628             sub delete :Chained('base') :Args {
629 0     0 0 0 my ($self, $c, @args) = @_;
630 0 0       0 my $template = $self->_resolve_template_name(@args)
631             or die "No template specified";
632            
633 0         0 local $self->{_current_context} = $c;
634            
635 0 0       0 $self->Access->template_deletable($template)
636             or return $self->_detach_response($c,403,"Delete template '$template' - Permission denied");
637            
638 0 0       0 die "Delete template '$template' - doesn't exists"
639             unless $self->_template_exists($c,$template);;
640            
641 0 0       0 $self->get_Provider->delete_template($template)
642             or die "Failed to delete template '$template'";
643              
644 0         0 return $self->_detach_response($c,200,"Deleted template '$template'");
645 4     4   4128 }
  4         8  
  4         18  
646              
647             sub _detach_response {
648 0     0   0 my ($self, $c, $status, $body, $content_type) = @_;
649             #$content_type ||= 'text/plain; charset=utf-8';
650 0 0       0 $c->response->content_type($content_type) if ($content_type);
651 0         0 $c->response->status($status);
652 0         0 $c->response->body($body);
653 0         0 return $c->detach;
654             }
655              
656             sub _render_template {
657 0     0   0 my ($self, $meth, $template, $c) = @_;
658            
659 0         0 my $TT = $self->$meth;
660 0         0 local $self->{_current_context} = $c;
661 0 0       0 local $self->{_div_wrap} = 1 if ($meth eq 'Template_wrap');
662 0         0 my $vars = $self->get_wrapped_tt_vars($template);
663 0         0 my $output;
664            
665             # TODO/FIXME: this is duplicate logic that has to be handled for the
666             # top-level template which doesn't seem to go through process() in Context:
667 0 0 0     0 $output = $self->Template_raw->context->_template_error_content(
668             $template, $TT->error, (
669             $self->is_editable_request($c) &&
670             $self->Access->template_writable($template)
671             )
672             ) unless $TT->process( $template, $vars, \$output );
673            
674 0         0 return $output;
675             }
676              
677             # Returns undef if the template is valid or the error
678             sub _get_template_error {
679 0     0   0 my ($self, $meth, $template, $c) = @_;
680 0         0 my $TT = $self->$meth;
681 0         0 local $self->{_current_context} = $c;
682 0 0       0 local $self->{_div_wrap} = 1 if ($meth eq 'Template_wrap');
683 0         0 my $vars = $self->get_wrapped_tt_vars($template);
684 0         0 my $output;
685 0         0 local $self->{_no_exception_error_content} = 1;
686 0 0       0 return $TT->process( $template, $vars, \$output ) ? undef : $TT->error;
687             }
688              
689             # Internal render function - designed to be called interactively
690             # from other parts of the application to render a template (i.e.
691             # not associated with a Template::Controller request)
692             #
693             # TODO: This function will replace/merge with $c->template_render
694             # in RapidApp::Role::CatalystApplication
695             sub template_render {
696 1     1 0 113 my ($self, $template, $vars, $c) = @_;
697 1   50     4 $vars ||= {};
698 1   33     3 $c ||= RapidApp->active_request_context;
699            
700             # The current context may not be available:
701             # see DummyAccess in RapidApp::Template::Access:
702 1 50       4 local $self->{_dummy_access} = 1 unless ($c);
703 1   33     6 local $self->{_current_context} = $c || $self->_app;
704            
705             # The get_template_vars() API in the Access class expects
706             # to have access to the catalyst context (i.e. request) so
707             # we only call it and merge it in if we have $c, which is
708             # optional in this method
709 1 50       5 %$vars = (%{ $self->get_wrapped_tt_vars($template) }, %$vars)
  1         7  
710             if ($c);
711            
712 1         33 my $TT = $self->Template_raw;
713              
714 1         2 my $out;
715 1 50       7 $TT->process($template,$vars,\$out) or die $TT->error;
716              
717 1         98 return $out;
718             }
719              
720              
721             # Wraps all CodeRef vars to cleanly catch exceptions that may be
722             # thrown by them. TT isn't able to handle them properly...
723             sub get_wrapped_tt_vars {
724 1     1 0 4 my ($self,$template) = @_;
725 1         35 my $vars = $self->Access->get_template_vars($template);
726            
727 1 50       4 die "Access class method 'get_template_vars()' didn't return a HashRef!"
728             unless (ref($vars) eq 'HASH');
729            
730 1         5 for my $var (keys %$vars) {
731 7 100       17 next unless (ref($vars->{$var}) eq 'CODE');
732 4         5 my $coderef = delete $vars->{$var};
733             $vars->{$var} = sub {
734 0     0   0 my @args = @_;
735 0         0 my $ret;
736             try {
737 0         0 $ret = $coderef->(@args);
738             }
739             catch {
740 0         0 my $err_msg = "!! EXCEPTION IN CODEREF TEMPLATE VARIABLE '$var': $_";
741            
742             # TODO/FIXME:
743             # We set the return value with the exception as a string (i.e. as content)
744             # instead of re-throwing because TT will display a useless and confusing
745             # error message, something like: "...Useless bare catch()..."
746 0         0 $ret = $err_msg;
747            
748             # We may not actually be able to see the error in the template rendering
749             # but at least it will be printed on the console (an exception here isn't
750             # actually a *Template* error, per-se ... its an error in the perl code
751             # that is called by this CodeRef)
752 0         0 warn RED.BOLD . $err_msg . CLEAR;
753 0         0 };
754 0         0 return $ret;
755 4         23 };
756             }
757            
758 1         11 return $vars;
759             }
760              
761             1;