import rt 3.8.7
[freeside.git] / rt / lib / RT / Template_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 #                                          <jesse@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     
98     unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
99         return ( 0, $self->loc('Permission Denied') );
100     }
101     return $self->SUPER::_Set( @_ );
102 }
103
104 =head2 _Value
105
106 Takes the name of a table column. Returns its value as a string,
107 if the user passes an ACL check, otherwise returns undef.
108
109 =cut
110
111 sub _Value {
112     my $self  = shift;
113
114     unless ( $self->CurrentUserHasQueueRight('ShowTemplate') ) {
115         return undef;
116     }
117     return $self->__Value( @_ );
118
119 }
120
121 =head2 Load <identifer>
122
123 Load a template, either by number or by name.
124
125 Note that loading templates by name using this method B<is
126 ambiguous>. Several queues may have template with the same name
127 and as well global template with the same name may exist.
128 Use L</LoadGlobalTemplate> and/or L<LoadQueueTemplate> to get
129 precise result.
130
131 =cut
132
133 sub Load {
134     my $self       = shift;
135     my $identifier = shift;
136     return undef unless $identifier;
137
138     if ( $identifier =~ /\D/ ) {
139         return $self->LoadByCol( 'Name', $identifier );
140     }
141     return $self->LoadById( $identifier );
142 }
143
144 =head2 LoadGlobalTemplate NAME
145
146 Load the global template with the name NAME
147
148 =cut
149
150 sub LoadGlobalTemplate {
151     my $self = shift;
152     my $name = shift;
153
154     return ( $self->LoadQueueTemplate( Queue => 0, Name => $name ) );
155 }
156
157 =head2 LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
158
159 Loads the Queue template named NAME for Queue QUEUE.
160
161 Note that this method doesn't load a global template with the same name
162 if template in the queue doesn't exist. THe following code can be used:
163
164     $template->LoadQueueTemplate( Queue => $queue_id, Name => $template_name );
165     unless ( $template->id ) {
166         $template->LoadGlobalTemplate( $template_name );
167         unless ( $template->id ) {
168             # no template
169             ...
170         }
171     }
172     # ok, template either queue's or global
173     ...
174
175 =cut
176
177 sub LoadQueueTemplate {
178     my $self = shift;
179     my %args = (
180         Queue => undef,
181         Name  => undef,
182         @_
183     );
184
185     return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
186
187 }
188
189 =head2 Create
190
191 Takes a paramhash of Content, Queue, Name and Description.
192 Name should be a unique string identifying this Template.
193 Description and Content should be the template's title and content.
194 Queue should be 0 for a global template and the queue # for a queue-specific 
195 template.
196
197 Returns the Template's id # if the create was successful. Returns undef for
198 unknown database failure.
199
200 =cut
201
202 sub Create {
203     my $self = shift;
204     my %args = (
205         Content     => undef,
206         Queue       => 0,
207         Description => '[no description]',
208         Type        => 'Action', #By default, template are 'Action' templates
209         Name        => undef,
210         @_
211     );
212
213     unless ( $args{'Queue'} ) {
214         unless ( $self->CurrentUser->HasRight(Right =>'ModifyTemplate', Object => $RT::System) ) {
215             return ( undef, $self->loc('Permission Denied') );
216         }
217         $args{'Queue'} = 0;
218     }
219     else {
220         my $QueueObj = new RT::Queue( $self->CurrentUser );
221         $QueueObj->Load( $args{'Queue'} ) || return ( undef, $self->loc('Invalid queue') );
222     
223         unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
224             return ( undef, $self->loc('Permission Denied') );
225         }
226         $args{'Queue'} = $QueueObj->Id;
227     }
228
229     my $result = $self->SUPER::Create(
230         Content     => $args{'Content'},
231         Queue       => $args{'Queue'},
232         Description => $args{'Description'},
233         Name        => $args{'Name'},
234     );
235
236     return ($result);
237
238 }
239
240 =head2 Delete
241
242 Delete this template.
243
244 =cut
245
246 sub Delete {
247     my $self = shift;
248
249     unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
250         return ( 0, $self->loc('Permission Denied') );
251     }
252
253     return ( $self->SUPER::Delete(@_) );
254 }
255
256 =head2 IsEmpty
257
258 Returns true value if content of the template is empty, otherwise
259 returns false.
260
261 =cut
262
263 sub IsEmpty {
264     my $self = shift;
265     my $content = $self->Content;
266     return 0 if defined $content && length $content;
267     return 1;
268 }
269
270 =head2 MIMEObj
271
272 Returns L<MIME::Entity> object parsed using L</Parse> method. Returns
273 undef if last call to L</Parse> failed or never be called.
274
275 Note that content of the template is UTF-8, but L<MIME::Parser> is not
276 good at handling it and all data of the entity should be treated as
277 octets and converted to perl strings using Encode::decode_utf8 or
278 something else.
279
280 =cut
281
282 sub MIMEObj {
283     my $self = shift;
284     return ( $self->{'MIMEObj'} );
285 }
286
287 =head2 Parse
288
289 This routine performs L<Text::Template> parsing on the template and then
290 imports the results into a L<MIME::Entity> so we can really use it. Use
291 L</MIMEObj> method to get the L<MIME::Entity> object.
292
293 Takes a hash containing Argument, TicketObj, and TransactionObj and other
294 arguments that will be available in the template's code. TicketObj and
295 TransactionObj are not mandatory, but highly recommended.
296
297 It returns a tuple of (val, message). If val is false, the message contains
298 an error message.
299
300 =cut
301
302 sub Parse {
303     my $self = shift;
304     my ($rv, $msg);
305
306
307     if ($self->Content =~ m{^Content-Type:\s+text/html\b}im) {
308         local $RT::Transaction::PreferredContentType = 'text/html';
309         ($rv, $msg) = $self->_Parse(@_);
310     }
311     else {
312         ($rv, $msg) = $self->_Parse(@_);
313     }
314
315     return ($rv, $msg) unless $rv;
316
317     my $mime_type   = $self->MIMEObj->mime_type;
318     if (defined $mime_type and $mime_type eq 'text/html') {
319         $self->_DowngradeFromHTML(@_);
320     }
321
322     return ($rv, $msg);
323 }
324
325 sub _Parse {
326     my $self = shift;
327
328     # clear prev MIME object
329     $self->{'MIMEObj'} = undef;
330
331     #We're passing in whatever we were passed. it's destined for _ParseContent
332     my ($content, $msg) = $self->_ParseContent(@_);
333     return ( 0, $msg ) unless defined $content && length $content;
334
335     if ( $content =~ /^\S/s && $content !~ /^\S+:/ ) {
336         $RT::Logger->error(
337             "Template #". $self->id ." has leading line that doesn't"
338             ." look like header field, if you don't want to override"
339             ." any headers and don't want to see this error message"
340             ." then leave first line of the template empty"
341         );
342         $content = "\n".$content;
343     }
344
345     my $parser = MIME::Parser->new();
346     $parser->output_to_core(1);
347     $parser->tmp_to_core(1);
348     $parser->use_inner_files(1);
349
350     ### Should we forgive normally-fatal errors?
351     $parser->ignore_errors(1);
352     # MIME::Parser doesn't play well with perl strings
353     utf8::encode($content);
354     $self->{'MIMEObj'} = eval { $parser->parse_data( \$content ) };
355     if ( my $error = $@ || $parser->last_error ) {
356         $RT::Logger->error( "$error" );
357         return ( 0, $error );
358     }
359
360     # Unfold all headers
361     $self->{'MIMEObj'}->head->unfold;
362
363     return ( 1, $self->loc("Template parsed") );
364
365 }
366
367 # Perform Template substitutions on the template
368
369 sub _ParseContent {
370     my $self = shift;
371     my %args = (
372         Argument       => undef,
373         TicketObj      => undef,
374         TransactionObj => undef,
375         @_
376     );
377
378     unless ( $self->CurrentUserHasQueueRight('ShowTemplate') ) {
379         return (undef, $self->loc("Permission Denied"));
380     }
381
382     if ( $self->IsEmpty ) {
383         return ( undef, $self->loc("Template is empty") );
384     }
385
386     my $content = $self->SUPER::_Value('Content');
387     # We need to untaint the content of the template, since we'll be working
388     # with it
389     $content =~ s/^(.*)$/$1/;
390     my $template = Text::Template->new(
391         TYPE   => 'STRING',
392         SOURCE => $content
393     );
394
395     $args{'Ticket'} = delete $args{'TicketObj'} if $args{'TicketObj'};
396     $args{'Transaction'} = delete $args{'TransactionObj'} if $args{'TransactionObj'};
397     $args{'Requestor'} = eval { $args{'Ticket'}->Requestors->UserMembersObj->First->Name }
398         if $args{'Ticket'};
399     $args{'rtname'}    = RT->Config->Get('rtname');
400     if ( $args{'Ticket'} ) {
401         my $t = $args{'Ticket'}; # avoid memory leak
402         $args{'loc'} = sub { $t->loc(@_) };
403     } else {
404         $args{'loc'} = sub { $self->loc(@_) };
405     }
406
407     foreach my $key ( keys %args ) {
408         next unless ref $args{ $key };
409         next if ref $args{ $key } =~ /^(ARRAY|HASH|SCALAR|CODE)$/;
410         my $val = $args{ $key };
411         $args{ $key } = \$val;
412     }
413
414
415     my $is_broken = 0;
416     my $retval = $template->fill_in(
417         HASH => \%args,
418         BROKEN => sub {
419             my (%args) = @_;
420             $RT::Logger->error("Template parsing error: $args{error}")
421                 unless $args{error} =~ /^Died at /; # ignore intentional die()
422             $is_broken++;
423             return undef;
424         }, 
425     );
426     return ( undef, $self->loc('Template parsing error') ) if $is_broken;
427
428     return ($retval);
429 }
430
431 sub _DowngradeFromHTML {
432     my $self = shift;
433     my $orig_entity = $self->MIMEObj;
434
435     my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
436     $new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
437     $new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
438
439     $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
440     $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
441     $orig_entity->make_multipart('alternative', Force => 1);
442
443     require HTML::FormatText;
444     require HTML::TreeBuilder;
445     my $tree = HTML::TreeBuilder->new_from_content(
446         $new_entity->bodyhandle->as_string
447     );
448     $new_entity->bodyhandle(MIME::Body::InCore->new(
449         \(scalar HTML::FormatText->new(
450             leftmargin  => 0,
451             rightmargin => 78,
452         )->format( $tree ))
453     ));
454     $tree->delete;
455
456     $orig_entity->add_part($new_entity, 0); # plain comes before html
457     $self->{MIMEObj} = $orig_entity;
458
459     return;
460 }
461
462 =head2 CurrentUserHasQueueRight
463
464 Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
465
466 =cut
467
468 sub CurrentUserHasQueueRight {
469     my $self = shift;
470     return ( $self->QueueObj->CurrentUserHasRight(@_) );
471 }
472
473 1;