RT 3.8.17
[freeside.git] / rt / lib / RT / Template_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 # Portions Copyright 2000 Tobias Brox <tobix@cpan.org> 
50
51 =head1 NAME
52
53   RT::Template - RT's template object
54
55 =head1 SYNOPSIS
56
57   use RT::Template;
58
59 =head1 DESCRIPTION
60
61
62 =head1 METHODS
63
64
65 =cut
66
67
68 package RT::Template;
69
70 use strict;
71 use warnings;
72 no warnings qw(redefine);
73
74 use Text::Template;
75 use MIME::Entity;
76 use MIME::Parser;
77
78 sub _Accessible {
79     my $self = shift;
80     my %Cols = (
81         id            => 'read',
82         Name          => 'read/write',
83         Description   => 'read/write',
84         Type          => 'read/write',    #Type is one of Action or Message
85         Content       => 'read/write',
86         Queue         => 'read/write',
87         Creator       => 'read/auto',
88         Created       => 'read/auto',
89         LastUpdatedBy => 'read/auto',
90         LastUpdated   => 'read/auto'
91     );
92     return $self->SUPER::_Accessible( @_, %Cols );
93 }
94
95 sub _Set {
96     my $self = shift;
97     my %args = (
98         Field => undef,
99         Value => undef,
100         @_,
101     );
102     
103     unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
104         return ( 0, $self->loc('Permission Denied') );
105     }
106
107     if (exists $args{Value}) {
108         if ($args{Field} eq 'Queue') {
109             if ($args{Value}) {
110                 # moving to another queue
111                 my $queue = RT::Queue->new( $self->CurrentUser );
112                 $queue->Load($args{Value});
113                 unless ($queue->Id and $queue->CurrentUserHasRight('ModifyTemplate')) {
114                     return ( 0, $self->loc('Permission Denied') );
115                 }
116             } else {
117                 # moving to global
118                 unless ($self->CurrentUser->HasRight( Object => RT->System, Right => 'ModifyTemplate' )) {
119                     return ( 0, $self->loc('Permission Denied') );
120                 }
121             }
122         }
123     }
124
125     return $self->SUPER::_Set( @_ );
126 }
127
128 =head2 _Value
129
130 Takes the name of a table column. Returns its value as a string,
131 if the user passes an ACL check, otherwise returns undef.
132
133 =cut
134
135 sub _Value {
136     my $self  = shift;
137
138     unless ( $self->CurrentUserHasQueueRight('ShowTemplate') ) {
139         return undef;
140     }
141     return $self->__Value( @_ );
142
143 }
144
145 =head2 Load <identifer>
146
147 Load a template, either by number or by name.
148
149 Note that loading templates by name using this method B<is
150 ambiguous>. Several queues may have template with the same name
151 and as well global template with the same name may exist.
152 Use L</LoadGlobalTemplate> and/or L<LoadQueueTemplate> to get
153 precise result.
154
155 =cut
156
157 sub Load {
158     my $self       = shift;
159     my $identifier = shift;
160     return undef unless $identifier;
161
162     if ( $identifier =~ /\D/ ) {
163         return $self->LoadByCol( 'Name', $identifier );
164     }
165     return $self->LoadById( $identifier );
166 }
167
168 =head2 LoadGlobalTemplate NAME
169
170 Load the global template with the name NAME
171
172 =cut
173
174 sub LoadGlobalTemplate {
175     my $self = shift;
176     my $name = shift;
177
178     return ( $self->LoadQueueTemplate( Queue => 0, Name => $name ) );
179 }
180
181 =head2 LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
182
183 Loads the Queue template named NAME for Queue QUEUE.
184
185 Note that this method doesn't load a global template with the same name
186 if template in the queue doesn't exist. THe following code can be used:
187
188     $template->LoadQueueTemplate( Queue => $queue_id, Name => $template_name );
189     unless ( $template->id ) {
190         $template->LoadGlobalTemplate( $template_name );
191         unless ( $template->id ) {
192             # no template
193             ...
194         }
195     }
196     # ok, template either queue's or global
197     ...
198
199 =cut
200
201 sub LoadQueueTemplate {
202     my $self = shift;
203     my %args = (
204         Queue => undef,
205         Name  => undef,
206         @_
207     );
208
209     return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
210
211 }
212
213 =head2 Create
214
215 Takes a paramhash of Content, Queue, Name and Description.
216 Name should be a unique string identifying this Template.
217 Description and Content should be the template's title and content.
218 Queue should be 0 for a global template and the queue # for a queue-specific 
219 template.
220
221 Returns the Template's id # if the create was successful. Returns undef for
222 unknown database failure.
223
224 =cut
225
226 sub Create {
227     my $self = shift;
228     my %args = (
229         Content     => undef,
230         Queue       => 0,
231         Description => '[no description]',
232         Type        => 'Action', #By default, template are 'Action' templates
233         Name        => undef,
234         @_
235     );
236
237     unless ( $args{'Queue'} ) {
238         unless ( $self->CurrentUser->HasRight(Right =>'ModifyTemplate', Object => $RT::System) ) {
239             return ( undef, $self->loc('Permission Denied') );
240         }
241         $args{'Queue'} = 0;
242     }
243     else {
244         my $QueueObj = new RT::Queue( $self->CurrentUser );
245         $QueueObj->Load( $args{'Queue'} ) || return ( undef, $self->loc('Invalid queue') );
246     
247         unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
248             return ( undef, $self->loc('Permission Denied') );
249         }
250         $args{'Queue'} = $QueueObj->Id;
251     }
252
253     my $result = $self->SUPER::Create(
254         Content     => $args{'Content'},
255         Queue       => $args{'Queue'},
256         Description => $args{'Description'},
257         Name        => $args{'Name'},
258     );
259
260     return ($result);
261
262 }
263
264 =head2 Delete
265
266 Delete this template.
267
268 =cut
269
270 sub Delete {
271     my $self = shift;
272
273     unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
274         return ( 0, $self->loc('Permission Denied') );
275     }
276
277     return ( $self->SUPER::Delete(@_) );
278 }
279
280 =head2 IsEmpty
281
282 Returns true value if content of the template is empty, otherwise
283 returns false.
284
285 =cut
286
287 sub IsEmpty {
288     my $self = shift;
289     my $content = $self->Content;
290     return 0 if defined $content && length $content;
291     return 1;
292 }
293
294 =head2 MIMEObj
295
296 Returns L<MIME::Entity> object parsed using L</Parse> method. Returns
297 undef if last call to L</Parse> failed or never be called.
298
299 Note that content of the template is UTF-8, but L<MIME::Parser> is not
300 good at handling it and all data of the entity should be treated as
301 octets and converted to perl strings using Encode::decode_utf8 or
302 something else.
303
304 =cut
305
306 sub MIMEObj {
307     my $self = shift;
308     return ( $self->{'MIMEObj'} );
309 }
310
311 =head2 Parse
312
313 This routine performs L<Text::Template> parsing on the template and then
314 imports the results into a L<MIME::Entity> so we can really use it. Use
315 L</MIMEObj> method to get the L<MIME::Entity> object.
316
317 Takes a hash containing Argument, TicketObj, and TransactionObj and other
318 arguments that will be available in the template's code. TicketObj and
319 TransactionObj are not mandatory, but highly recommended.
320
321 It returns a tuple of (val, message). If val is false, the message contains
322 an error message.
323
324 =cut
325
326 sub Parse {
327     my $self = shift;
328     my ($rv, $msg);
329
330
331     if ($self->Content =~ m{^Content-Type:\s+text/html\b}im) {
332         local $RT::Transaction::PreferredContentType = 'text/html';
333         ($rv, $msg) = $self->_Parse(@_);
334     }
335     else {
336         ($rv, $msg) = $self->_Parse(@_);
337     }
338
339     return ($rv, $msg) unless $rv;
340
341     my $mime_type   = $self->MIMEObj->mime_type;
342     if (defined $mime_type and $mime_type eq 'text/html') {
343         $self->_DowngradeFromHTML(@_);
344     }
345
346     return ($rv, $msg);
347 }
348
349 sub _Parse {
350     my $self = shift;
351
352     # clear prev MIME object
353     $self->{'MIMEObj'} = undef;
354
355     #We're passing in whatever we were passed. it's destined for _ParseContent
356     my ($content, $msg) = $self->_ParseContent(@_);
357     return ( 0, $msg ) unless defined $content && length $content;
358
359     if ( $content =~ /^\S/s && $content !~ /^\S+:/ ) {
360         $RT::Logger->error(
361             "Template #". $self->id ." has leading line that doesn't"
362             ." look like header field, if you don't want to override"
363             ." any headers and don't want to see this error message"
364             ." then leave first line of the template empty"
365         );
366         $content = "\n".$content;
367     }
368
369     my $parser = MIME::Parser->new();
370     $parser->output_to_core(1);
371     $parser->tmp_to_core(1);
372     $parser->use_inner_files(1);
373
374     ### Should we forgive normally-fatal errors?
375     $parser->ignore_errors(1);
376     # MIME::Parser doesn't play well with perl strings
377     utf8::encode($content);
378     $self->{'MIMEObj'} = eval { $parser->parse_data( \$content ) };
379     if ( my $error = $@ || $parser->last_error ) {
380         $RT::Logger->error( "$error" );
381         return ( 0, $error );
382     }
383
384     # Unfold all headers
385     $self->{'MIMEObj'}->head->unfold;
386     $self->{'MIMEObj'}->head->modify(1);
387
388     return ( 1, $self->loc("Template parsed") );
389
390 }
391
392 # Perform Template substitutions on the template
393
394 sub _ParseContent {
395     my $self = shift;
396     my %args = (
397         Argument       => undef,
398         TicketObj      => undef,
399         TransactionObj => undef,
400         @_
401     );
402
403     unless ( $self->CurrentUserHasQueueRight('ShowTemplate') ) {
404         return (undef, $self->loc("Permission Denied"));
405     }
406
407     if ( $self->IsEmpty ) {
408         return ( undef, $self->loc("Template is empty") );
409     }
410
411     my $content = $self->SUPER::_Value('Content');
412     # We need to untaint the content of the template, since we'll be working
413     # with it
414     $content =~ s/^(.*)$/$1/;
415     my $template = Text::Template->new(
416         TYPE   => 'STRING',
417         SOURCE => $content
418     );
419
420     $args{'Ticket'} = delete $args{'TicketObj'} if $args{'TicketObj'};
421     $args{'Transaction'} = delete $args{'TransactionObj'} if $args{'TransactionObj'};
422     $args{'Requestor'} = eval { $args{'Ticket'}->Requestors->UserMembersObj->First->Name }
423         if $args{'Ticket'};
424     $args{'rtname'}    = RT->Config->Get('rtname');
425     if ( $args{'Ticket'} ) {
426         my $t = $args{'Ticket'}; # avoid memory leak
427         $args{'loc'} = sub { $t->loc(@_) };
428     } else {
429         $args{'loc'} = sub { $self->loc(@_) };
430     }
431
432     foreach my $key ( keys %args ) {
433         next unless ref $args{ $key };
434         next if ref $args{ $key } =~ /^(ARRAY|HASH|SCALAR|CODE)$/;
435         my $val = $args{ $key };
436         $args{ $key } = \$val;
437     }
438
439
440     my $is_broken = 0;
441     my $retval = $template->fill_in(
442         HASH => \%args,
443         BROKEN => sub {
444             my (%args) = @_;
445             $RT::Logger->error("Template parsing error: $args{error}")
446                 unless $args{error} =~ /^Died at /; # ignore intentional die()
447             $is_broken++;
448             return undef;
449         }, 
450     );
451     return ( undef, $self->loc('Template parsing error') ) if $is_broken;
452
453     return ($retval);
454 }
455
456 sub _DowngradeFromHTML {
457     my $self = shift;
458     my $orig_entity = $self->MIMEObj;
459
460     my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
461     $new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
462     $new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
463
464     $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
465     $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
466     $orig_entity->make_multipart('alternative', Force => 1);
467
468     require HTML::FormatText;
469     require HTML::TreeBuilder;
470     require Encode;
471     # need to decode_utf8, see the doc of MIMEObj method
472     my $tree = HTML::TreeBuilder->new_from_content(
473         Encode::decode_utf8($new_entity->bodyhandle->as_string)
474     );
475     $new_entity->bodyhandle(MIME::Body::InCore->new(
476         \(scalar HTML::FormatText->new(
477             leftmargin  => 0,
478             rightmargin => 78,
479         )->format( $tree ))
480     ));
481     $tree->delete;
482
483     $orig_entity->add_part($new_entity, 0); # plain comes before html
484     $self->{MIMEObj} = $orig_entity;
485
486     return;
487 }
488
489 =head2 CurrentUserHasQueueRight
490
491 Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
492
493 =cut
494
495 sub CurrentUserHasQueueRight {
496     my $self = shift;
497     return ( $self->QueueObj->CurrentUserHasRight(@_) );
498 }
499
500 1;